杜克大学-R-语言数据科学笔记-全-
杜克大学 R 语言数据科学笔记(全)
001:欢迎与课程概述 🎼



在本节课中,我们将学习课程的基本介绍、数据科学的整体流程,以及我们将要使用的核心工具。课程将从最基础的“Hello World”程序开始,逐步引导你进入数据科学的世界。

大家好,欢迎来到本课程。我的名字是Minet Tiinkaandel,我将担任“R语言数据科学”专项课程的讲师。本专项课程的第一门课是“数据可视化与转换”。让我们开始吧。

“Hello World”通常是初学者编写的第一个程序。由于本课程将涉及大量编码,我们也将从“Hello World”开始。
但在开始之前,让我们先谈谈数据科学。数据科学是一门令人兴奋的学科,它能让你将原始数据转化为理解、洞察和知识。进行数据科学的方法和工具有很多,在本课程中,你将学习以“整洁”的方式进行数据科学。关于“整洁”在此语境下的具体含义,我们稍后会详细讨论。具体来说,这是一门强调统计思维的数据科学入门课程。
然而,在接触数据科学内容之前,让我们先回顾一些常见问题。
以下是关于课程背景和工具的几个常见问题:
- 问:本课程需要什么数据科学背景?
- 答:不需要任何背景。但是,它要求你有学习的意愿和与数据工作的动力。
- 问:我们会进行计算吗?
- 答:是的,我们会。
- 问:我们将学习什么计算语言?为什么是R语言而不是X语言?
- 答:这是个好问题。数据科学家使用许多语言。事实上,许多数据科学家使用多种语言,一部分分析用一种语言,另一部分用另一种语言。在本课程中,我们选择坚持使用R语言。
现在,让我们谈谈软件。
你们中的一些人可能熟悉电子表格程序中按行和列组织的数据。电子表格是存储表格数据的普遍工具,有些人也在其中进行数据分析,但我们不会这样做。相反,我们将使用R语言。
这是我研究生时期开始学习R时的工作界面。坦率地说,那看起来有点令人生畏。如今,大部分使用R工作的人都使用RStudio,你也将在本课程中学习它。RStudio的窗口一开始可能看起来令人生畏,但这没关系。我们将在整个课程中逐步介绍各个窗格及其使用方法。

我们已经介绍了用于进行数据分析的软件,但一个端到端的数据分析包含哪些内容呢?让我们来看一下。



上图是来自《R for Data Science》一书的数据科学生命周期图,你将在本课程中阅读这本书。该图概述了数据科学的许多阶段。
我们从导入数据开始,数据可能来自电子表格、数据库,或从网络上抓取。
然后现实情况出现了:你几乎永远无法获得完全原始、并且恰好为你将要进行的分析完美组织好的数据。因此,你通常需要对其进行整理和转换,使其为分析做好准备。
我们通常从探索性分析开始,这包括可视化和数据摘要。
然后,你可能会应用建模技术,用你的数据进行推断和预测。
这通常会揭示出你希望通过进一步转换和可视化来解决的数据方面的问题。因此,你会花一些时间在这个循环中,以理解你的数据。
最终,你要么对你的数据分析感到满意,要么遇到了截止日期。所以最后,你需要传达你的结果。
这里有一个来自谷歌趋势指数的数据示例,它衡量了与旅行等主题相关的特定搜索词(如图所示)的受欢迎程度随时间的变化。我们看到旅行相关搜索词的流行度呈现出周期性或季节性模式。
这促使我们回到包含日期的原始数据。我们可能希望基于这些日期计算并创建一个新的“季节”列,然后在后续分析中使用该变量作为指标。
这是一个例子,说明可视化和一些建模结果(即叠加在可视化上的红色平滑曲线)如何促使我们进行进一步的数据转换,并让我们保持在那个探索循环中。
然后,正如我所说,你要么遇到截止日期并分析数据,要么充分分析数据后需要传达结果。但这感觉就像研究了很长时间后,直到最后一刻才动笔写学期论文,这不是我们想要提倡的习惯。
因此,我们将在理解数据的循环中加入沟通阶段,边分析数据边撰写结果。但我们需要一种无需太多上下文切换就能做到这一点的方法。
因此,我们将在计算笔记本中进行分析,特别是Quarto文档,它允许你在同一文档中编写和运行代码,并撰写叙述性文字。
我们还可以在数据科学生命周期中看到另一个阶段,它囊括了所有其他阶段:编程。
它被单独列出来,不是因为它比其他阶段不重要,而是因为它某种程度上管理着所有阶段。所有这些阶段都需要一点编程。
所有这些阶段加在一起,可能需要相当长的时间来学习。因此,在本专项课程的第一门课中,我们将主要关注数据可视化和转换阶段,并讨论使用R进行数据科学的工具。


本节课总结


在本节课中,我们一起学习了课程的欢迎介绍和数据科学的基本流程。我们明确了课程不需要先验知识,但需要学习的热情。我们了解到本课程将使用R和RStudio作为核心工具,并初步认识了数据科学生命周期的各个阶段:导入、整理、转换、探索、建模和沟通。在接下来的课程中,我们将重点深入数据可视化和数据转换这两个核心环节。
002:我的首次数据可视化-联合国投票 📊


在本节课中,我们将学习如何打开并运行一个R Markdown文档,通过修改代码来探索不同国家在联合国大会上的投票模式,并生成你的第一个数据可视化图表。
概述
我们将使用一个名为“UN votes”的R Markdown文件。这个分析旨在探索各国在联合国大会上的投票模式如何随时间演变,以及它们在特定议题上的投票相似性或差异性。我们将通过修改现有代码,替换图表中的国家,来实践如何在不完全理解所有代码的情况下,通过调整关键部分来获得所需结果。
数据与工具包
我们将使用几个R包来完成分析。以下是所需包及其简要说明:
- tidyverse:用于数据整理和可视化。
- scales:用于美化坐标轴标签。
- DT:用于创建交互式数据表格,方便我们查找和确认国家的准确拼写。
- unvotes:该数据包包含了联合国投票数据,我们将使用它提供的数据框。
在开始分析前,我们首先加载这些包并准备数据。数据来源于unvotes包,我们通过连接(join)包内的多个数据框,创建了一个名为un_votes的单一数据框。
初始可视化与代码解读
上一节我们加载了数据,本节中我们来看看初始的可视化结果及其背后的代码逻辑。
运行初始代码后,我们得到一张折线图。X轴代表年份,Y轴代表投“赞成”票的百分比。图表显示了三个国家(土耳其、英国、美国)在六个不同议题(如殖民主义、军备控制、人权等)上的投票趋势。
以下是生成该图表的代码核心部分。我们不需要完全理解每一行,但需要识别出决定显示哪些国家的关键代码段:
# 示例代码结构(非完整代码)
un_votes %>%
filter(country %in% c(“Turkey”, “United Kingdom”, “United States”)) %>%
# ... 后续的数据处理和绘图代码
可以看到,filter(country %in% c(...)) 这一行通过一个列表指定了要筛选出的国家。我们的目标就是修改这个列表。
实践挑战:替换国家
现在,我们的挑战是修改代码,将图表中的一个国家(例如土耳其)替换为你感兴趣的国家。
关键步骤是定位到筛选国家的代码行,并更新国家名称列表。例如,将 “Turkey” 替换为 “France”。修改后,重新渲染(Render)整个文档,观察可视化图表是否相应地更新。
修改并渲染后,新的图表会显示法国、英国和美国的投票模式。我们可以观察到,例如在巴勒斯坦冲突议题上,法国和英国的投票模式比之前土耳其的投票模式更为相似。
总结

本节课中我们一起学习了如何操作一个R Markdown分析项目。我们实践了打开文档、首次渲染以确保无错误、识别关键代码段(特别是用于筛选数据的部分)、修改代码以替换分析对象(国家),并重新渲染文档以查看更新后的可视化结果。

这个练习的核心在于建立信心:即使面对尚未完全掌握的代码,也能通过有根据的尝试和观察结果来探索数据并完成简单的定制化分析。
003:R与RStudio



🎼

在本课程以及整个专项课程中,我们将使用一系列现代数据科学工具。在本视频中,我们将认识其中两个:R与RStudio。
在认识它们之前,我们先回顾一下学习目标。到本课程结束时,你将能够从数据中获得洞察。更具体地说,是使用现代编程工具和技术从数据中获得洞察。再具体一点,是使用现代编程工具和技术,通过文学化编程可重复地从数据中获得洞察。或者,让我们说得更具体一些,是使用现代编程工具和技术,通过文学化编程和版本控制可重复地从数据中获得洞察。
不过,在本视频中,我们将专注于使用现代编程工具和技术从数据中获得洞察,也就是第一个要点。我们将使用R和RStudio来实现。
什么是R?
R是一种开源的统计编程语言。它也是一个用于统计计算和图形的环境。
什么是RStudio?
RStudio是R的一个便捷界面,称为IDE,即集成开发环境。例如,我可以说我在RStudio IDE中编写R代码。使用RStudio并非R编程的必需条件,但它被R程序员和数据科学家广泛使用。
一个关于R与RStudio的恰当比喻是汽车引擎与仪表盘。R就像引擎盖下的发动机。RStudio是一个仪表盘,通过它你可以与引擎以及R包进行交互。
什么是R包?
包是可重复R代码的基本单元。它们包含可重用的R函数、这些函数的文档以及示例数据。它们是R社区扩展R的方式。截至2024年4月,CRAN上有超过20,000个R包可用。CRAN是综合R存档网络。我们将使用其中一小部分但非常重要的包。
接下来,让我们来参观一下R和RStudio。
RStudio界面导览
好的,这就是RStudio。我们在这里看到了什么?
首先,我们有R控制台。当我们打开RStudio时,可以看到我们可以访问R。它会告诉我正在使用的R版本。例如,如果你最近没有更新R版本,你这里的数字可能不同。在控制台中,我可以进行简单的算术运算。我可以输入类似 2 + 2 的代码,然后得到结果 4。这当然并不令人兴奋,但你可以看到我们可以在这里做这些简单的事情。
你还可以做一些事情,这些事情虽然也可以用计算器完成,但感觉开始有些不同了,那就是我们可以将值赋给对象。例如,我可以输入 x <- 2。一旦我这样做,我就可以在环境选项卡中看到我创建的对象及其值。然后我也可以用那个 x 进行算术运算。我一直在运算符之间加空格,但这完全是可选的。我也可以省略空格,写成 x+2。我喜欢加空格,这样代码看起来更宽松,更容易阅读,但在R中空格是可选的。
在R中处理数据
现在让我们开始在R中实际处理数据。我们将在课程中反复使用的一个包包含了一些关于企鹅的数据。要加载一个包,包名是 palmerpenguins。所以,要加载这个包,我会输入 library(palmerpenguins)。当我这样做时,你会发现好像什么都没发生。在使用R编程,以及其他语言时,需要习惯的一点是:没有响应并不总是意味着出错了。事实上,如果出错了,你会看到错误、警告或消息。在这种情况下,包已经加载了,只是我们没有要求R返回任何东西给我们,所以它只是说“我已经完成了需要做的事情,准备接受下一个命令”。我们知道它准备接受下一个命令,因为我们可以看到那个小小的回车符和光标在屏幕上闪烁。
在这个特定的包中,有一个名为 penguins 的数据框。我可以输入数据框的名称。我碰巧知道这个数据框存在,并且可以这样查看它。它被打印到我的屏幕上。看起来并不是所有内容都适合我的屏幕,例如,有一个名为 year 的变量我们实际上看不到。通常,当你有一个这样的数据框时,最好能以类似电子表格的视图查看它。所以,我还可以做一件事,输入 View(penguins)。这将在数据查看器中打开它。在处理数据时,我喜欢能够在数据查看器中打开并查看它。如果你有一个非常大的数据集,这个操作可能会很耗时,比如有数百万行或数千列。但当我们开始学习用R进行数据科学时,我们将处理的数据集虽然不是很小,但规模适中,应该总是可以在这里打开它们。
一旦在查看器中打开数据,就可以进行过滤等操作。例如,你可以开始与数据交互,你并没有改变它,只是查看它。可以通过点击列名按字母或数字顺序排列列。
访问数据框中的变量
我们如何访问这些列中的一个,也就是数据框中的一个变量呢?格式总是:数据框名称 + 美元符号 + 你感兴趣的变量名。当你在RStudio中编程时,它有一些辅助功能。它会显示数据框中存在的变量名。所以,如果我忘了企鹅的体重记录为 body_mass_g,我可以用箭头键从那里选择它,然后这样查看它们。
R是这样查看数据的好方法吗?我不想看到每只企鹅的体重。我可能想对这些数据做些什么,比如计算企鹅的平均体重。你知道它们有多大吗?让我们试着算一下。
计算平均值
在统计学中,当我们说“平均”时,我们指的是均值。在R中计算均值的函数就是 mean。你怎么会知道这个呢?你不一定知道。你可能会谷歌搜索“如何在R中计算均值”。许多(虽然不是全部)这些函数的命名都很贴切,所以你可能会猜一下,然后通过谷歌找到方法。
在R中,一旦我们输入函数名,后面就会加上括号。我们想要应用函数的内容放在括号里。我们想计算 penguins$body_mass_g 的均值。我们得到了一个 NA 作为响应。每当你得到像这样的意外结果时,最好先检查一下:我是不是做错了什么?同时也要快速思考:我想知道 mean 函数是如何工作的,可能出了什么问题?这意味着我们需要查看文档。我们有两种方法可以做到这一点。
我们可以在控制台中输入一个问号,然后输入函数名。这将在帮助菜单中弹出。或者,让我回到帮助菜单的主页,在帮助菜单的搜索栏中,我可以输入函数名。你可以看到它提供了一个函数列表。它提供给你的列表取决于你安装的包。所以,如果这是你使用R的第一天,你可能安装的包较少,呈现的选项也可能较少。但 mean 函数在基础R中,对所有人可用,我们不需要加载任何包就可以使用它。我们可以通过这种方式获得帮助。
一旦进入帮助页面,你将能看到函数名、它所在的包(如前所述,这是基础R)、函数功能的标题以及描述。如果我们往下看,我们可以看到函数的参数。第一个参数是 x,这是我们想要计算均值的对象。但我们也可以看到,我们有选项可以计算修剪均值,即修剪掉数据框中的极端值。还有一个名为 na.rm 的选项,它说这是一个逻辑值,指示在计算进行之前是否应去除 NA 值。我们可以看到,na.rm 参数的默认值是 FALSE。所以,如果我们的数据框中有任何 NA 值,也就是任何我们没有记录体重的企鹅,这个参数是关于我们是否应该在计算均值之前将它们移除。我们很可能想这样做。所以,让我们来试试看。我可以再次输入函数,添加这个参数。或者,让我回去,我可以用键盘上的上箭头键滚动浏览我最近编写的代码。所以,为了节省一些打字,我将回到这里,然后输入一个逗号,表示我要添加一个新参数:na.rm = TRUE。
现在我看到这些企鹅的平均体重大约是4202克。所以大约是4.2公斤。
回顾总结
作为回顾,我们可以使用R控制台作为计算器进行简单的算术运算或对象赋值,这些对象会显示在环境选项卡中。我们可以使用 library 函数加载包。我们可以使用 View 函数在数据查看器中查看数据集。我们可以使用美元符号访问数据框中的变量,格式是:数据框名称 + 美元符号 + 变量名。像 mean 这样的R函数后面跟着括号,括号里是我们想要应用函数的对象。当一个函数给出意外结果(比如我们得到的 NA)或错误时,最好的首选去处是函数文档,可以通过帮助窗格或输入问号后跟函数名来访问。函数的其他参数用逗号分隔。
当前R基础要点列表
现在让我们回顾一个简短的R基础要点列表:
- 函数:通常是动词,后面跟着它们将要应用的对象,这些对象放在括号里,例如“对这个做那个”或“对这个,对那个,用那些”。额外的参数用逗号分隔。
- 包:使用
install.packages函数安装,使用library函数加载(每个会话一次)。 - 数据框中的列或变量:使用美元符号访问,格式为
data_frame$variable_name。 - 对象文档:使用问号访问,例如
?mean。
关于Tidyverse
最后,让我们谈谈 tidyverse。Tidyverse是一个为数据科学设计的、具有特定理念的R包集合。这些包将是我们在本课程和整个专项课程中最常使用的。所有这些包共享一个底层哲学和一套通用语法。


004:Quarto

📖 概述
在本节课中,我们将学习一个名为 Quarto 的重要工具。Quarto 是一个开源的科学与技术出版系统,它能帮助我们创建可重复的数据分析报告。我们将了解什么是可重复的数据分析,以及如何使用 Quarto 来实现这一目标。
🔍 什么是可重复的数据分析?
上一节我们介绍了课程目标,本节中我们来看看实现这些目标的关键工具。课程结束时,你将能够使用现代编程工具和技术,以文学化编程的方式,从数据中获得可重复的见解。这正是 Quarto 发挥作用的地方。
那么,数据分析的“可重复性”具体指什么?简单来说,它意味着在相同的数据上运行相同的分析,应该得到相同的结果。这听起来显而易见,但实际上并非总能实现。分析所使用的工具和工作流程的选择至关重要。
例如,如果你在 Excel 这样的电子表格软件中进行数据分析,并决定因为数据错误或离群值而删除某些观测行,你可能会直接从电子表格中删除这些行。这种做法很难(甚至不可能)被记录和复现。一旦删除,就无法追溯。
相反,如果你使用支持脚本化步骤的工具(例如编写 R 代码),原始数据将保持不变,相应的行会被代码过滤掉,并且这一步骤会记录在你的代码中。这样,无论是其他分析师还是几个月后的你自己,都可以从原始数据开始,准确地复现整个分析过程。
因此,我们的目标是:
- 短期:确保图表和结果可以从代码和数据中复现。代码应准确执行你的意图,并且除了“做了什么”,还应清楚“为什么这么做”。
- 长期:编写的代码应能适用于其他数据,并可扩展用于其他任务。
🛠️ 如何实现可重复性?
为了实现这些目标,我们需要:
- 脚本化分析:我们将使用 R 语言。
- 文学化编程:将代码、叙述文字和输出整合在单一文档中,使两者密不可分。这正是 Quarto 的核心功能。
- 版本控制:对于更复杂的项目,我们将使用 Git 和 GitHub。
本视频将重点介绍第二点:文学化编程,并使用 Quarto 来实现它。
🧭 Quarto 导览
Quarto 允许你生成完全可重复的报告。每次渲染文档时,分析都会从头开始运行。你的代码放在称为代码块或代码单元的区域,叙述性文字则放在代码块之外。在 RStudio 中编写 Quarto 文档,你还能获得类似 Google Docs 或 MS Word 的熟悉编辑体验。
让我们来实际浏览一下 Quarto。
文档结构
一个 Quarto 文档(扩展名为 .qmd)主要包含两部分:
1. YAML 元数据
文档顶部,由三个短横线 --- 包围的区域称为 YAML 头部。这里存放文档的元数据,例如标题、作者等。Quarto 会为这些字段应用预定义的样式。
示例:
---
title: "我的企鹅分析"
author: "你的名字"
---
2. 内容区域
YAML 头部之后是文档的主要内容,包括普通文本和代码块。
代码块
以下是代码块的核心组成部分。
代码块结构:
一个典型的代码块如下所示:
```{r}
# 这里是你的R代码
library(tidyverse)
```
代码块选项:
你可以在花括号 {} 内为代码块设置选项,以控制其行为。
- 标签:
{r my-label}为代码块命名,便于在长文档中导航。 echo:控制是否在输出中显示代码。echo=FALSE会隐藏代码,只显示结果。message:控制是否显示包加载等信息。message=FALSE会隐藏这些消息。
示例:
```{r load-data, message=FALSE, echo=FALSE}
# 这个代码块加载数据,但在最终报告中不显示代码和加载信息
library(palmerpenguins)
```
渲染文档
点击 “Render” 按钮,Quarto 会执行文档中的所有代码块,并将结果(文本、图表、表格)整合成一个格式美观的输出文档(如 HTML、PDF)。渲染时,Quarto 只使用文档内部定义的对象和加载的包,与 RStudio 控制台中的全局环境是隔离的。
行内代码
你还可以在叙述文字中直接嵌入代码,动态生成内容。使用 `r ` 语法插入行内 R 代码。
示例:
- 输入:
企鹅数据集共有 \r nrow(penguins)` 行。` - 输出:企鹅数据集共有 344 行。
这样做的好处是,如果底层数据(penguins)发生变化,这个数字会自动更新,无需手动修改。
⚠️ 重要注意事项:环境隔离
一个常见且重要的陷阱是:Quarto 文档的环境与 RStudio 控制台的环境是分离的。
这意味着,所有用于生成结果的变量定义、数据操作和包加载,都必须在 Quarto 文档内部的代码块中完成。当你点击“Render”时,Quarto 只会访问文档内部的内容,而不会识别你在控制台中随意创建的“游离”对象。
错误示例:
- 在控制台输入:
x <- 2 - 在 Quarto 文档中写:
`r x*3` - 渲染时会报错:
object 'x' not found
正确做法:
必须在 Quarto 文档的某个代码块中定义 x,例如:
```{r}
x <- 2
```
然后才能在行内代码或后续代码块中使用 `x`。
---
## 📚 课程中的应用
在本课程中,Quarto 将是核心工具:
* **代码练习**:每次练习都将基于一个 Quarto 模板文档开始。随着课程的深入,模板提供的“脚手架”会逐渐减少,以鼓励你独立编写更多代码。
* **创建材料**:值得一提的是,本课程的所有幻灯片和编程练习也都是使用 Quarto 创建的。
---
## 🎯 总结
本节课我们一起学习了:
1. **可重复数据分析**的重要性:确保分析过程透明、结果可复现。
2. **Quarto 工具**:一个用于文学化编程和创建可重复报告的强大系统。
3. **Quarto 文档结构**:包括 YAML 元数据、代码块和叙述文本。
4. **核心操作**:如何编写代码块、设置选项、插入行内代码以及渲染文档。
5. **关键原则**:牢记 Quarto 文档的环境隔离性,所有分析步骤都应在文档内编码完成。

掌握 Quarto 将帮助你构建专业、透明且易于维护的数据分析工作流。
# 005:数据可视化入门



在本节课中,我们将介绍数据可视化的基本概念,探讨其重要性,并概述如何利用R语言进行数据可视化。我们将从理解数据集的结构开始,逐步学习如何通过可视化来探索和发现数据中的模式与异常。
## 数据集的结构
在深入数据可视化之前,我们首先需要正式理解数据集的概念。
关于数据集有两个重要的术语:
* 每一行代表一个**观测值**。
* 每一列代表一个**变量**。
我们正在查看一个名为`Star Wars`的数据集,它包含了《星球大战》宇宙中角色的信息。每一行是一个角色,每一列是该角色的一个属性。例如,第一行是卢克·天行者。让我们快速查看这一行。
我们知道关于卢克的一些信息:他的名字、身高(172厘米)、体重(77公斤)、头发颜色(金色)、出生年份等等。此外,我们知道的关于他的一些信息是单一条目,例如眼睛颜色是蓝色,皮肤颜色是白皙,物种是人类。然而,其他一些信息是列表,因为他在该特定变量上有多个条目,例如他出演过的电影、驾驶过的车辆和星际飞船。
本课程将重点讨论每个观测值只有一个条目的变量,如身高、体重、眼睛颜色、皮肤颜色、物种等。在本专业课程的后续部分,我们将更多地讨论列表列,即一列中每个观测值有多个条目的情况。目前,我们暂时搁置这些列,因为在能够处理和可视化它们之前,我们需要了解更多编程知识。
当你开始接触一个新数据集时,一个有用的函数是`glimpse`函数。它不仅会报告数据集中的行数和列数,还会给我们一个列的列表。特别是如果你有一个包含许多列的宽数据集,这是列出所有列的好方法。这个视图还告诉我们列的类型,例如,`CHR`代表字符型,`I`代表整数型,`DBL`代表双精度型等。我们将在课程后面讨论正式的数据类型和类。但这个输出还显示了每个变量的前几个观测值,这对于了解数据也很有帮助。
当你开始分析一个新数据集时,应该问的第一个问题是这个数据集有多少行和多少列,每一行代表什么,每一列代表什么。数据字典或数据文档是找到这些问题答案的好地方。这个特定的《星球大战》数据集是随一个R包分发的,具体来说是`dplyr`包,我们很快就会更正式地介绍它。由于它是随包分发的,其文档可以像任何R函数的文档一样访问,使用问号后跟你想了解更多信息的名称。文档告诉我们关于数据的整体情况,并对每个变量进行了描述。
这里我再介绍一个新概念:**包**。包是可复现R代码的基本单元。它们包括可重用的R函数、描述如何使用它们的文档以及像《星球大战》数据集这样的示例数据。`dplyr`是用于数据整理的最流行的R包之一。
好的,回到《星球大战》。我们还能如何回答这些初步的数据问题,例如行数和列数?一些方便的函数是`nrow`、`ncol`和`dim`。它们的名字很有启发性:`nrow`代表行数,`ncol`代表列数,`dim`代表数据集的维度,其输出首先是行数,然后是列数。请注意,在每种情况下,函数名后面都跟着括号,括号里是你想应用该函数的对象名称。因此,我们会这样表述这些函数:`nrow` of `starwars`,它有87行;`ncol` of `starwars`,14列;`dim` of `starwars`,87乘以14。

## 探索性数据分析

现在我们对数据框在R中的表示方式以及如何开始查看它们有了一些了解,让我们开始探索它们。
探索性数据分析,通常简称为EDA,是一种分析数据集以总结其主要特征的方法。这通常是可视化的,这是我们首先要关注的;但我们也可能计算汇总统计量,这是我们接下来要关注的;并且可视化和汇总都可能需要在分析的这一阶段之前或期间进行数据整理、操作和转换。
我们的第一个探索是《星球大战》角色的质量和身高。我们想回答以下问题:你如何描述《星球大战》角色质量与身高之间的关系?哪些其他变量能帮助我们理解那些不遵循整体趋势的数据点?那个不太高但真的很胖的角色是谁?
为了回答这个问题,让我们看一下Y轴为质量、X轴为身高的可视化图。通常,对于人类和动物来说,身高和体重是正相关的,但《星球大战》宇宙中有一些非人类角色,比如机器人,谁知道他们的身高和体重是如何关联的。因此,虽然我们可以发现数据中大多数观测值存在这种正相关关系,但由于一个极端的异常值,整体趋势很难看到,它将其余的数据挤压到了图的底部。
那么,这个不太高但真的很胖的角色是谁?如果你是《星球大战》粉丝,你可能已经猜到了。但即使你对《星球大战》知之甚少,你也会认出这个小角色。原来是贾巴,不太高,但真的非常胖。可视化帮助我们识别出与其他观测值不同的那个观测值,而对数据的深入挖掘揭示了该观测值。
约翰·图基(John Tukey)写了很多关于探索性数据分析的文章,他的一句话在这里很贴切:“简单的图表为数据分析师的大脑带来的信息比任何其他工具都多。”
数据可视化是探索性数据分析的重要组成部分,从高层次上讲,它是数据的视觉表示的创建和研究。有许多用于可视化数据的工具,R只是其中之一;在R内部,也有许多制作数据可视化的方法和系统。`ggplot2`是另一个R包,也是我们将要使用的工具。

## 为什么需要数据可视化

我们很快就会讲到如何操作,但让我们先简单谈谈为什么,我们为什么要进行可视化?
看看这个数据集,它是一个著名的数据集,叫做安斯库姆四重奏。实际上有四个数据集堆叠在一起,每个数据集有两个变量:X和Y。这张幻灯片上有一堆代码,你很快就会学到更多,但简而言之,它为每个数据集计算了一些汇总统计量。让我们现在专注于输出,而不是代码。你看到了什么?每个数据集的X值的平均值是相同的,Y值的平均值、标准差甚至X和Y之间的相关性也是如此。基于这些汇总统计量,你认为这些数据集如何比较?它们对我来说看起来是相同的。
但让我们先不要急于下结论,首先将数据可视化。再次强调,现在专注于输出,而不是代码。这些散点图揭示了一些非常不同的东西;这些数据集看起来一点也不相似。
如果我还没有说服你可视化是探索性分析的关键部分,这里还有两个例子。第一个例子来自一项调查,大学生被问及他们第一次接吻时的年龄。首先想想你期望他们的回答分布会是什么样子。然后检查这个图。你看到什么不寻常的地方了吗?许多回答都在我期望的位置。但那些在更年轻年龄的回答呢?这很可能表明调查问题设计得不好。一些受访者一定将问题理解为浪漫的吻,而另一些人则理解为任何吻,比如妈妈给宝宝的吻。如果我们只是查看汇总统计量,比如第一次接吻的平均年龄,我们将无法捕捉到这个数据特征。
这是另一个例子。同一组学生被问及他们每天访问Facebook的次数。再次,想想你期望他们的回答分布会是什么样子。然后看看这个图。人们是如何报告较低与较高的Facebook访问次数的?那些报告较低值的人似乎给出了精确的答案,比如一天三次或八次,而那些报告较高值的人似乎提供了四舍五入的答案,比如一天100次。这又是一个无法从汇总统计量中识别出来的特征,但可视化使其一目了然。
这些例子中的每一个都说明了我们为什么需要可视化数据。接下来是如何操作。
## 总结

在本节课中,我们一起学习了数据集的基本结构,包括观测值和变量的概念。我们介绍了探索性数据分析(EDA)的重要性,并通过具体案例看到了数据可视化在揭示数据模式、识别异常值和理解数据特征方面的强大作用。可视化不仅仅是绘制图表,它是我们理解数据、提出问题和发现洞见的关键第一步。在接下来的课程中,我们将开始学习如何使用`ggplot2`包在R中创建这些强大的可视化图形。
# 006:使用ggplot2逐步构建图表

在本节课中,我们将学习如何使用`ggplot2`包来可视化数据。我们将以企鹅数据集为例,从零开始,逐步构建一个完整的图表,并了解`ggplot2`语法的核心结构。


## 概述`ggplot2`包
`ggplot2`是Tidyverse生态系统中的数据可视化包。要使用它,我们需要加载`tidyverse`库。
```r
library(tidyverse)
```
`ggplot2`中的“gg”代表“图形语法”,其灵感来源于Leland Wilkinson的著作《图形语法》。学习该包及其函数的实用资源是其官方网站:`ggplot2.tidyverse.org`。

`ggplot2`的代码结构通常遵循以下模式:
1. `ggplot()`函数,指定数据和美学映射。
2. 添加`geom_*()`层,定义数据的几何形状。
3. 添加其他层(如标签、主题)来完善图表。
## 代码结构示例
我们曾见过一张《星球大战》角色身高与体重的散点图。以下是其代码结构:
```r
ggplot(data = starwars, mapping = aes(x = height, y = mass)) +
geom_point()
```
代码解析:
* `ggplot()`:第一个参数是数据(`data = starwars`),第二个参数是美学映射(`mapping = aes(x = height, y = mass)`),定义了x轴和y轴对应的变量。
* `+ geom_point()`:添加一个几何对象层,指定用点来表示数据。
* 警告信息提示有28个包含缺失值(NA)的观测值未被绘制。
## 逐步构建企鹅数据图表
现在,让我们将这套方法应用于企鹅数据。我们将使用`palmerpenguins`包中的数据,该数据包含了帕尔默群岛三种企鹅(阿德利、帽带、巴布亚)的体型测量值。
首先,加载数据并查看其结构:

```r
library(palmerpenguins)
glimpse(penguins)
```
数据包含8个变量(列),344行观测(只企鹅)。变量包括喙长、喙深、鳍状肢长度、体重、物种、岛屿、性别和年份。

我们的目标是绘制一张散点图:x轴为喙深(毫米),y轴为喙长(毫米),用点代表每只企鹅,并根据其物种着色。最终图表还将包含标题、副标题和图注。
### 第一步:创建画布
我们从`ggplot()`函数开始,指定要使用的数据集。
```r
ggplot(data = penguins)
```
运行这行代码会生成一个空白的画布,等待我们添加数据。
### 第二步:添加美学映射
接下来,我们定义哪些变量映射到图表的哪个视觉属性(美学)上。这里,我们将`bill_depth_mm`映射到x轴,将`bill_length_mm`映射到y轴。
```r
ggplot(data = penguins, mapping = aes(x = bill_depth_mm, y = bill_length_mm))
```
此时,图表坐标轴已根据变量范围和合理断点进行了设置。
### 第三步:添加几何对象
现在,我们需要指定如何用几何形状表示数据。我们使用`geom_point()`来添加点层。
```r
ggplot(data = penguins, mapping = aes(x = bill_depth_mm, y = bill_length_mm)) +
geom_point()
```
图表上出现了代表数据的点,可以看到大致有三个点簇。
### 第四步:按物种着色
为了区分物种,我们将`species`变量映射到颜色(`color`)这个美学属性上。这需要在`aes()`函数内完成。
```r
ggplot(data = penguins, mapping = aes(x = bill_depth_mm, y = bill_length_mm, color = species)) +
geom_point()
```
现在,点按物种着色,并且图表自动生成了图例来说明颜色与物种的对应关系。
### 第五步:添加标签
数据已就位,现在添加标签使图表更易读。我们使用`labs()`函数来设置标题、副标题、坐标轴标签和图例标题。
```r
ggplot(data = penguins, mapping = aes(x = bill_depth_mm, y = bill_length_mm, color = species)) +
geom_point() +
labs(
title = "Bill Depth and Length",
subtitle = "Measurements for Palmer Penguins",
x = "Bill Depth (mm)",
y = "Bill Length (mm)",
color = "Penguin Species"
)
```
### 第六步:添加数据来源说明
我们还可以在`labs()`中添加`caption`参数来注明数据来源,它会自动显示在图表底部。
```r
ggplot(data = penguins, mapping = aes(x = bill_depth_mm, y = bill_length_mm, color = species)) +
geom_point() +
labs(
title = "Bill Depth and Length",
subtitle = "Measurements for Palmer Penguins",
x = "Bill Depth (mm)",
y = "Bill Length (mm)",
color = "Penguin Species",
caption = "Data source: palmerpenguins package"
)
```
### 第七步:优化颜色方案
最后,为了确保色觉障碍者也能清晰区分颜色,我们使用`scale_color_viridis_d()`函数应用一个专为色觉友好设计的离散调色板。
```r
ggplot(data = penguins, mapping = aes(x = bill_depth_mm, y = bill_length_mm, color = species)) +
geom_point() +
labs(
title = "Bill Depth and Length",
subtitle = "Measurements for Palmer Penguins",
x = "Bill Depth (mm)",
y = "Bill Length (mm)",
color = "Penguin Species",
caption = "Data source: palmerpenguins package"
) +
scale_color_viridis_d()
```
## 关于参数名的补充说明
在R语言中,通常可以省略函数前两个参数的名称。对于`ggplot()`函数,这两个参数是`data`和`mapping`。因此,更常见的简洁写法如下:

```r
ggplot(penguins, aes(x = bill_depth_mm, y = bill_length_mm, color = species)) +
geom_point()
```
## 总结
本节课中,我们一起学习了使用`ggplot2`构建图表的核心流程:
1. 使用`ggplot()`初始化图表并指定数据和基本美学映射。
2. 使用`geom_*()`函数添加几何对象层来展示数据。
3. 使用`labs()`函数添加和修改各种标签。
4. 使用`scale_*()`函数调整颜色、坐标轴等标度。

通过从空白画布开始,逐步添加映射、几何对象、标签和优化选项,我们最终完成了一个信息丰富且美观的企鹅数据散点图。这种分层叠加的语法是`ggplot2`强大且灵活的关键。
# 007:初探帕尔默企鹅数据集 🐧




在本节课中,我们将首次动手操作帕尔默企鹅数据集。我们将探索企鹅的喙部尺寸(喙的深度和长度)如何随物种变化,并思考如果不考虑物种因素会发生什么。
我们将使用两个R包:`tidyverse`包(提供强大的数据处理和可视化功能)和`palmerpenguins`包(包含我们需要的数据集)。我们将要可视化的数据集名为 `penguins`。
## 加载包与数据
首先,我们加载必要的包并查看数据。以下是加载包的代码块:
```r
library(tidyverse)
library(palmerpenguins)
```
加载`tidyverse`包时会显示一些信息,提示我们它加载了哪些核心包(如`dplyr`、`ggplot2`)以及可能存在的函数名冲突。例如,`dplyr`包中的`filter`函数会“覆盖”基础R中`stats`包的`filter`函数。这意味着之后调用`filter()`时,R会优先使用`dplyr`的版本。如果想使用特定包的函数,可以使用`包名::函数名()`的语法(如`stats::filter()`)。为了使输出更简洁,我们可以通过设置代码块选项`message = FALSE`来隐藏这些加载信息。
接下来,我们使用`glimpse()`函数快速查看`penguins`数据集的结构。

```r
glimpse(penguins)
```

输出显示,该数据集包含344只企鹅的观测记录,共有8个变量,包括物种、岛屿、喙长、喙深等信息。为代码块添加有意义的标签(如`glimpse-data`)有助于在冗长的文档中快速导航和调试。

## 可视化探索:不考虑物种

上一节我们加载了数据,本节中我们来看看如何创建可视化图表。首先,我们创建一个散点图来探索所有企鹅的喙深与喙长之间的关系,并添加一条最佳拟合线。

以下是创建该图的代码:
```r
ggplot(data = penguins, aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point() +
geom_smooth(method = "lm", se = FALSE)
```
* **`ggplot(...)`**: 初始化绘图,指定数据源为`penguins`,并将`bill_length_mm`映射到x轴,`bill_depth_mm`映射到y轴。
* **`geom_point()`**: 添加点图层,用散点表示数据。
* **`geom_smooth(method = "lm", se = FALSE)`**: 添加平滑图层。`method = "lm"`指定使用线性模型拟合一条直线(最佳拟合线)。`se = FALSE`用于关闭表示不确定性的置信带。



生成的图表显示,喙长与喙深之间存在**微弱的负相关关系**,即随着喙的长度增加,其深度似乎略有减少。数据点围绕拟合线的散布较广。

## 可视化探索:按物种着色



上一节我们看到了一个总体的负相关趋势,但这有些反直觉。本节中我们来看看当引入物种信息后,关系是否会发生变化。现在,我们创建第二个散点图,这次根据物种对数据点进行着色,并为每个物种分别添加最佳拟合线。
以下是更新后的代码:
```r
ggplot(data = penguins, aes(x = bill_length_mm, y = bill_depth_mm, color = species)) +
geom_point() +
geom_smooth(method = "lm", se = FALSE)
```
* **`aes(..., color = species)`**: 在美学映射中新增`color = species`,这意味着数据点的颜色和后续图层的颜色将根据`species`变量的不同值(阿德利企鹅、巴布亚企鹅、帽带企鹅)进行区分。
新的图表清晰地分成了三个簇,每个物种一个。关键的变化是:**在每个物种内部,喙长与喙深呈现出明确的正相关关系**,且相关性中等偏强。每个物种的数据点更紧密地围绕在各自的拟合线周围。
## 分析与总结:辛普森悖论
对比两个可视化结果,我们遇到了统计学中一个经典的现象——**辛普森悖论**。它描述的是这样一种情况:当忽略某个重要变量(此处为`species`)时,两个变量(`bill_length_mm`和`bill_depth_mm`)之间呈现出某种关系(负相关);而一旦将这个“混杂变量”纳入考虑,群体内部的关系却完全相反(正相关)。
这并不是说第一个分析是错误的,而是它**不完整**。物种是理解企鹅喙部形态的关键因素。不同物种的企鹅,其喙的尺寸范围本就不同。当把所有数据混合在一起时,物种间的差异掩盖了物种内部真实的正相关关系。

因此,我们更应相信第二个分析的结果。在进行数据分析时,始终保持对潜在混杂变量的警觉至关重要。如果可能,咨询相关领域的专家也能帮助我们识别这些关键变量。
本节课中我们一起学习了:
1. 如何加载`tidyverse`和`palmerpenguins`包并查看数据。
2. 如何使用`ggplot2`创建散点图并添加线性拟合线。
3. 如何通过颜色美学将第三个变量(物种)引入可视化。
4. 认识了**辛普森悖论**,并理解了在数据分析中考虑潜在混杂变量的重要性。
# 008:图形语法 🎨

在本节课中,我们将学习支撑 `ggplot2` 绘图的理论基础——图形语法。图形语法是一种工具,它让我们能够精确地描述图形的各个组成部分。`ggplot2` 包是 `tidyverse` 的一部分,它在 R 语言中实现了图形语法。使用 `ggplot2`,你可以通过逐层叠加的方式创建多种多样的图形。
这些图层依次是:**数据**、**美学映射**、**几何对象**、**分面**、**统计变换**、**坐标系**,最后是**主题**。
---

## 第一层:数据 📊

数据层是绘图的基础,它为你提供了绘制数据的“画布”。`ggplot2` 代码的一个巧妙之处在于,每一层都会生成一个有效的图形,即使它只是一个像这里展示的空图。
---
## 第二层:美学映射 🎨
我们将花一些时间讨论美学映射。美学是绘图元素的特征,可以被映射到数据中的特定变量。例如,颜色、形状、大小或透明度(alpha)。换句话说,美学映射决定了数据如何被视觉化呈现。
以下是几种常见的美学映射示例:
* **颜色映射到物种**:每个物种等级由不同的自动选择的颜色表示。
* **形状映射到岛屿**:不同岛屿的数据点使用不同形状。
* **大小映射到体重**:这会影响图中点的大小。但请注意,过度使用可能导致图形混乱、难以阅读。因此,能够映射并不意味着应该映射,你需要判断这种映射是否合理并产生了有用的可视化效果。最好的判断方法就是亲自尝试。
* **透明度映射到鳍长**:当数据点大量重叠时,这非常有用,它可以显示数据密度的高低区域。
我们一直在使用“映射”这个术语。它特指将美学属性与数据框中的变量关联起来,因此美学属性的取值由数据中的值决定。
你也可以直接“设置”一个美学属性为固定值,例如,将所有点设置为红色,或将它们的大小调大一点。
**映射**与**设置**的区别:
* **映射**:基于数据中变量的值来决定点的大小、透明度等。它写在 `aes()` 函数内。
* **设置**:不基于数据变量,仅根据你的意愿来决定点的大小、透明度等。它写在 `geom_*()` 函数内。
这个区别在本课程和你自己的数据可视化实践中会经常出现。
---
## 第三层:几何对象 📐
上一节我们介绍了美学映射,本节中我们来看看几何对象。几何对象是数据点的视觉表示,是“geometric objects”的简称。`geom_*` 函数用于向图形添加几何对象,例如 `geom_point`、`geom_histogram`、`geom_boxplot` 等等。
每个 `geom` 都会为图形添加一个新图层,因此你可以将它们叠加起来。
以下是几何对象的示例:

* 我们仅用点来表示数据,以制作散点图,因此使用 `geom_point()`。
* 我们添加了另一个几何对象层,用一条拟合数据的平滑曲线来表示相同的数据。

你很快会学到更多几何对象。
---
## 第四层:分面 🧩

接下来是分面。什么是分面?我们为什么需要它?分面是较小的图形,每个图形显示数据的不同子集。它们通常也被称为“小倍数图”,对于探索条件关系和处理大数据非常有用。

以下是一个分面散点图的示例,按列分面为岛屿,按行分面为物种。
要对图形进行分面,我们添加一个分面层。在这个例子中,我们使用了 `facet_grid()` 函数,按两个变量将图形分面成一个网格。
* `species ~ island` 表示物种在行,岛屿在列。
* `island ~ species` 表示岛屿在行,物种在列。
你不必死记这个语法,我自己也经常搞混。只需尝试一种方式,如果不喜欢,再尝试另一种。
如果只想按单个变量分面,我们则使用 `facet_wrap()` 函数,它的语法有点特别:`~ species`,即波浪号后跟你想分面的变量。然后你可以控制这些分面的布局方式,例如,这里我们通过设置 `ncol = 1` 参数让它们排成一列。
---
## 第五、六层:统计变换与坐标系 📈
第五层和第六层是统计变换和坐标系。它们稍微高级一些,我们现在不深入讨论,但很快就会接触到。

---

## 第七层:主题 🖌️
最后是主题层。主题控制图形的非数据元素。你可以使用 `theme_*()` 函数选择预定义的主题(我们稍后会看到一个例子),或者通过 `theme()` 函数精细控制单个主题元素。
以下是主题设置的示例:
* 我们使用 `theme_dark()` 为图形添加了一个深色主题。
* 我们使用 `theme()` 函数,设置 `legend.position = "bottom"`,将图例位置改到了图形底部。
在本课程中,我们会在相关内容出现时重点介绍常用的主题元素。请记住,你始终可以通过 `?theme`(即问号后跟函数名)来访问函数文档,以了解所有可用选项。探索所有这些函数的文档是练习使用 `ggplot2` 进行可视化的绝佳方式。
---

在本节课中,我们一起学习了图形语法的七个核心图层:数据、美学映射、几何对象、分面、统计变换、坐标系和主题。理解这些图层如何协同工作,是使用 `ggplot2` 创建有效且美观的数据可视化的关键。
# 009:数据转换语法

## 概述
在本节课中,我们将学习数据转换的语法。我们将从理解数据框和Tibble开始,然后介绍一个用于数据转换的核心R包——`dplyr`,并学习如何使用管道操作符构建清晰的数据处理流程。
## 数据框与Tibble
上一节我们介绍了图形语法,本节中我们来看看数据转换的语法。
与可视化数据时一样,在转换数据时,我们也总是从一个数据框开始。但在输出中,我们经常看到另一个术语:Tibble。
因此,让我们首先讨论数据框和Tibble。为了演示这些概念,我们将使用一个名为`flights`的数据框,它包含了2013年从纽约市三个机场起飞的所有航班的信息。熟悉的人会知道,这三个机场是JFK、拉瓜迪亚和纽瓦克。
让我们加载`tidyverse`包和包含航班数据的`nycflights13`包。
我们可以查看`flights`中的行数。它有超过30万行,这意味着当年从纽约三个机场之一出发的航班数量,以及19列,意味着我们知道关于每个航班的19件事。
预览数据也有助于了解变量及其类型。例如,我们可以看到我们知道诸如到达和起飞时间、任何延误等信息。
使用`names`函数,我们还可以获取所有变量名的列表。
另一个方便的函数是`head`,它默认会打印出数据框的前六行。请注意,输出中也提到了Tibble这个词。
那么,什么是Tibble?Tibble是R数据框的一个“有主见”的版本。换句话说,所有Tibble都是数据框,但并非所有数据框都是Tibble。
但更明确地说,Tibble和数据框之间有两个主要区别:打印方式和更严格的子集选取。
关于打印,当你打印一个Tibble时,R只会显示数据框的前10行,并且屏幕上能容纳的所有列也会显示出来,同时还会显示每列的类型。
当你打印一个`data.frame`(即原始的R数据框)时,R会尝试显示所有数据,即所有行和所有列。我的意思是,如果数据集太大,可能会导致崩溃。因此,Tibble只打印前10行和屏幕上能容纳的列的行为,是对大型数据潜在崩溃的一种保护。
关于子集选取,Tibble提供更严格的子集选取。这是什么意思?如果你尝试选取一个Tibble中不存在的变量,例如,这里我在`flights`数据框中寻找一个名为`apple`的变量,我们知道它不存在。R不仅返回`NULL`,还会给出一个警告,提示未知或未初始化的列`apple`,基本上就是说这个列在数据框中不存在。
另一方面,对于`data.frame`(原始的R数据框),你只会得到`NULL`,而不是信息丰富的警告。
## 介绍dplyr包
好了,是时候介绍一个新包`dplyr`了。我们之前详细讨论过`ggplot2`,从它的名字来看,很明显它与绘图有关。`dplyr`不那么明显,但我想一旦你听到解释,它可能就有意义了。简而言之,它是“data plier”的缩写,就像你想“处理”你的数据一样。
`dplyr`是`tidyverse`中用于数据转换的主要包,包括从数据整理、清理到汇总的任何操作。关于`dplyr`提供的所有功能,一个很好的资源是其官方网站:`dplyr.tidyverse.org`。
## 行操作与列操作
在处理数据时,将你可能想要对数据进行的操作视为行操作与列操作是很有用的。
例如,使用`dplyr`,你可以根据位置用`slice`选择行,或根据列值用`filter`选择行,或用`arrange`对行排序,或用`sample_n`随机抽取行的子集。
例如,这里我获取`flights`数据框,然后切片其前五行。
或者这里我获取`flights`数据框,然后筛选出`dest`(目的地)等于`RDU`的行,`RDU`是服务于杜克大学所在地区的罗利-达勒姆机场。数据中有8163个这样的航班。
## 管道操作符
这是一个暂停并解决房间里的大象的好时机:一个我们尚未明确定义的非常方便的工具——管道操作符。
管道操作符,即一个竖线后跟一个大于号,是一个操作符。换句话说,它是一个将信息从一个过程传递到另一个过程的工具。
我们主要在数据管道中使用管道操作符,将上一行代码的输出作为下一行代码的第一个输入。用英语阅读代码时,我们可以将管道操作符发音为“然后”。
例如:获取`flights`数据框,然后筛选出`dest`等于`RDU`的行。
## 应用:回答一个数据问题
好了,让我们给数据转换的介绍一个明确的目的,并尝试用一个数据管道来回答以下问题:**从纽约市机场飞往RDU的航班中,哪家航空公司的平均延误时间最长?**
所以我们的目标是获得一个像这样的数据框,显示航空公司的承运商代码及其平均到达延误时间,按降序排列。
我们获取`flights`数据框。
然后筛选出飞往`RDU`的航班。
然后按`carrier`(承运商)分组。
然后计算每个承运商的平均到达延误,我们用`summarize`来做这件事。
然后按平均到达延误降序排列。
结果出来了。承运商代码为`UA`(即美联航)的航班,在2013年从纽约市机场飞往RDU的平均延误时间为56分钟。
## 列操作与分组操作
到目前为止我们讨论了行操作,但我们也可以用`dplyr`进行列操作。例如,我们可以`select`列,这会改变是否包含某列;我们可以`rename`列,这会改变列的名称;或者我们可以`mutate`,这会改变现有列的值或创建新列。
此外,我们还可以使用`dplyr`对行组进行操作。因此,`group_by`对变量的每个值分别执行计算;`summarize`将一组折叠成单行;`count`计算一个或多个变量的唯一值数量。
我们将在接下来的代码练习视频中使用这些函数。

## 总结
本节课中,我们一起学习了数据转换的基础语法。我们首先区分了数据框和Tibble,了解了Tibble在打印和子集选取上的优势。接着,我们介绍了`dplyr`包,它是`tidyverse`中进行数据转换的核心工具。我们学习了如何使用管道操作符`%>%`(读作“然后”)将多个数据处理步骤清晰地连接起来,构建数据管道。最后,我们通过一个实际案例,演示了如何组合使用`filter`、`group_by`、`summarize`和`arrange`等函数来回答一个具体的数据问题。掌握这些基础操作是进行高效数据清洗、整理和分析的关键。
# 010:航班数据与管道操作 ✈️


在本节课中,我们将要学习如何使用R语言中的`tidyverse`包,特别是`dplyr`包,来处理和分析航班数据。我们将重点学习如何通过管道操作符`%>%`将多个数据处理步骤串联起来,形成一个清晰、高效的数据处理流程。

## 概述
我们将使用`nycflights13`包中的`flights`数据集,该数据集包含了2013年从纽约市三个机场(拉瓜迪亚机场、肯尼迪机场和纽瓦克机场)起飞的所有航班信息。通过一系列练习,我们将学习如何选择、筛选、排序、汇总和转换数据。

## 加载数据包
首先,我们需要加载必要的R包。`tidyverse`包用于数据处理和可视化,`nycflights13`包则提供了我们要使用的航班数据。
```r
library(tidyverse)
library(nycflights13)
```
## 探索数据集
在开始分析之前,我们先来了解一下数据集的基本结构。
以下是查看数据集概览的方法:
```r
glimpse(flights)
```
通过`glimpse`函数的输出,我们可以看到`flights`数据框有336,776行,每一行代表2013年从纽约市三个机场起飞的一个航班。数据集中包含19个变量,例如年份、月份、起飞时间、到达时间以及延误情况等。
为了在文档中动态地嵌入行数,我们可以使用`nrow`函数:
```r
`r nrow(flights)`
```
这样,当数据更新时,行数也会自动更新。
接下来,我们查看数据集中所有变量的名称:
```r
names(flights)
```
如果想了解每个变量的具体含义,可以查看数据集的帮助文档:
```r
?flights
```
## 选择数据列(`select`函数)
现在,我们开始使用`dplyr`包中的函数来处理数据。首先,我们学习如何选择特定的数据列。


以下是使用`select`函数选择特定变量的示例:
* **选择特定列**:创建一个只包含`dep_delay`(起飞延误)和`arr_delay`(到达延误)的数据框。
```r
flights %>%
select(dep_delay, arr_delay)
```
* **排除特定列**:创建一个包含除`dep_delay`外所有其他列的数据框。在R中,使用`!`符号表示“非”。
```r
flights %>%
select(!dep_delay)
```
* **选择列范围**:创建一个包含从`year`到`dep_delay`(包含两端)的所有列的数据框。使用冒号`:`操作符。
```r
flights %>%
select(year:dep_delay)
```
* **使用选择辅助函数**:选择所有名称中包含字符串`"arr_"`的变量。`contains()`是一个非常有用的辅助函数。
```r
flights %>%
select(contains("arr_"))
```
## 选择数据行(`slice`函数)
上一节我们介绍了如何操作数据列,本节中我们来看看如何操作数据行。`slice`函数允许我们根据行的位置(索引)来选择数据。
以下是使用`slice`函数选择特定行的示例:
* **显示前五行**:
```r
flights %>%
slice(1:5)
```
* **显示最后两行**:我们可以手动计算行数,但更稳健的方法是使用`slice_tail`函数。
```r
flights %>%
slice_tail(n = 2)
```
## 排序数据行(`arrange`函数)
我们不仅可以选择行,还可以改变行的顺序。`arrange`函数用于根据一个或多个变量对数据框的行进行排序。
以下是使用`arrange`函数排序数据的示例:
* **按起飞延误升序排列**:延误时间最短的航班排在最前面。
```r
flights %>%
arrange(dep_delay)
```
* **按起飞延误降序排列**:延误时间最长的航班排在最前面。使用`desc()`函数实现降序。
```r
flights %>%
arrange(desc(dep_delay))
```
* **组合操作**:找到延误时间最长的航班,并只显示其飞机尾号、承运商和起飞延误信息。这结合了`arrange`、`slice`和`select`函数。
```r
flights %>%
arrange(desc(dep_delay)) %>%
slice_head(n = 1) %>%
select(tailnum, carrier, dep_delay)
```
## 筛选数据行(`filter`函数)
除了按位置选择行,我们还可以根据条件来筛选行。`filter`函数用于保留满足指定逻辑条件的行。
以下是使用`filter`函数筛选数据的示例,其中使用了R的逻辑运算符(如`==`, `!=`, `<`, `>`等):
* **筛选目的地为RDU的航班**:
```r
flights %>%
filter(dest == "RDU")
```
* **组合条件筛选**:筛选目的地为RDU**且**到达延误小于0(即提前到达)的航班。使用逗号或`&`表示“且”。
```r
flights %>%
filter(dest == "RDU", arr_delay < 0)
```
* **使用`%in%`和`|`操作符**:筛选目的地是RDU或GSO,**并且**到达延误或起飞延误小于0的航班。
```r
flights %>%
filter(dest %in% c("RDU", "GSO"),
arr_delay < 0 | dep_delay < 0)
```




## 数据汇总(`count`和`summarize`函数)
到目前为止,我们的操作结果都是一个数据框,其中每一行仍然代表一个航班。接下来,我们将学习如何对数据进行汇总,得到更高层次的统计信息。
### 使用`count`进行计数
`count`函数可以快速计算每个组中的观测值数量。
以下是使用`count`函数汇总数据的示例:
* **创建目的地频率表**:
```r
flights %>%
count(dest)
```
* **按计数排序**:添加`sort = TRUE`参数,使结果按航班数量降序排列。
```r
flights %>%
count(dest, sort = TRUE)
```
* **找出航班最少的月份**:首先按月份计数,然后使用`arrange`排序找到最小值。
```r
flights %>%
count(month) %>%
arrange(n)
```
* **找出航班最多的日期**:按月份和日期的组合进行计数,并排序。
```r
flights %>%
count(month, day, sort = TRUE)
```
### 使用`summarize`进行统计
`summarize`(或`summarise`)函数可以将数据框压缩为单行摘要统计。
以下是使用`summarize`函数计算统计量的示例:
* **计算所有航班的平均到达延误**:注意处理缺失值(NA)。
```r
flights %>%
summarize(mean_arr_delay = mean(arr_delay, na.rm = TRUE))
```
* **按组计算统计量**:结合`group_by`函数,计算每个月的平均到达延误。
```r
flights %>%
group_by(month) %>%
summarize(mean_arr_delay = mean(arr_delay, na.rm = TRUE))
```
* **计算每个起飞机场的中位起飞延误**:
```r
flights %>%
group_by(origin) %>%
summarize(median_dep_delay = median(dep_delay, na.rm = TRUE))
```
## 创建新变量(`mutate`函数)
如果数据集中现有的变量不满足分析需求,我们可以使用`mutate`函数来创建新的变量或修改现有变量。
以下是使用`mutate`函数转换数据的示例:
* **单位转换与计算**:将空中飞行时间(分钟)转换为小时,并计算航班的速度(英里/小时)。
```r
flights %>%
mutate(air_time_hour = air_time / 60,
mph = distance / air_time_hour)
```
* **计算比例**:首先计算每月的航班数,然后计算每月航班数占总航班数的比例。
```r
flights %>%
count(month) %>%
mutate(prop = n / sum(n))
```
## 总结
本节课中我们一起学习了如何使用`dplyr`包中的核心函数和管道操作符`%>%`来处理`flights`航班数据集。我们掌握了以下技能:
1. **选择列**:使用`select`函数。
2. **选择行**:使用`slice`函数。
3. **排序行**:使用`arrange`函数。
4. **筛选行**:使用`filter`函数和逻辑运算符。
5. **汇总数据**:使用`count`进行计数,使用`group_by`和`summarize`进行分组统计。
6. **创建新变量**:使用`mutate`函数。

通过将这些函数用管道`%>%`连接起来,我们可以构建出清晰、可读且强大的数据处理流程,这是用R进行数据科学分析的基础。
# 011:分类数据的可视化与摘要统计



## 概述
在本节课中,我们将学习如何对分类数据进行可视化和摘要统计。我们将介绍单变量、双变量和多变量分析的基本概念,并使用一个实际的贷款数据集来演示如何创建条形图、分段条形图以及计算频率和相对频率表。
---
## 变量类型与数据介绍
上一节我们介绍了数据分析的基本类型。本节中我们来看看如何区分不同类型的变量。
首先,根据涉及的变量数量,我们可以将分析分为三类:
* **单变量数据分析**:分析单个变量的分布。本节课我们将主要关注分类变量的单变量分析。
* **双变量数据分析**:分析两个变量之间的关系。
* **多变量数据分析**:同时分析多个变量之间的关系,通常是在控制其他变量的条件下研究两个变量间的关系。
多变量分析最接近现实情况。例如,你一天的能量水平不仅与睡眠时长有关,还与你摄入的卡路里、运动量等其他变量有关。我们的目标是在掌握了单变量和双变量分析工具后,逐步过渡到多变量分析。
既然我们提到要关注分类变量,那么也需要定义一下什么是分类变量。
* **数值型变量**可以根据是否能取无限个值,进一步分为**连续型**或**离散型**。
* **分类变量**则可以根据其类别是否具有自然顺序,分为**有序分类**或**无序分类**。
本节课使用的数据来自Lending Club,这是一个允许个人向其他个人提供贷款的平台。数据集包含了数千笔通过该平台发放的贷款信息。请注意,这些数据是已发放的贷款,而非贷款申请。
让我们加载`tidyverse`和`openintro`包。`openintro`包包含了我们将要使用的名为`loans_full_schema`的数据。
```r
library(tidyverse)
library(openintro)
glimpse(loans_full_schema)
```
数据集包含10000行(代表10000笔贷款)和55个变量(每笔贷款的55条信息)。我们将只使用其中的8个变量。
```r
loans <- loans_full_schema %>%
select(loan_amount, interest_rate, term, grade, state, annual_income, homeownership, debt_to_income)
glimpse(loans)
```
以下是这8个变量的描述:
1. `loan_amount`:贷款金额(美元),**数值型连续变量**。
2. `interest_rate`:年利率(百分比),**数值型连续变量**。
3. `term`:贷款期限(月),**数值型离散变量**。
4. `grade`:贷款等级(A到G),代表贷款质量和偿还可能性,**有序分类变量**。
5. `state`:借款人所在州,**无序分类变量**。
6. `annual_income`:年收入(美元),**数值型连续变量**。
7. `homeownership`:房屋所有权状态(自有、抵押、租赁),**无序分类变量**。
8. `debt_to_income`:债务收入比,**数值型连续变量**。
---
## 分类数据的可视化
了解了变量类型后,现在让我们来可视化一些分类数据。
### 单变量条形图
对于单个分类变量,我们可以使用条形图。我们将使用`geom_bar()`函数。
```r
ggplot(loans, aes(x = homeownership)) +
geom_bar()
```
`ggplot2`会自动计算拥有抵押、自有房屋和租赁房屋的人数,并根据这些计数确定条形的高度。
### 双变量分段条形图
如果想引入另一个分类变量,我们可以使用分段条形图。具体做法是根据第二个变量的值对现有条形进行填充。
```r
ggplot(loans, aes(x = homeownership, fill = grade)) +
geom_bar()
```
现在,`ggplot2`会同时计算每个房屋所有权组的人数,以及每个组内不同贷款等级的人数。
### 处理数值型分类变量
让我们尝试用`term`(期限)变量来填充条形图。
```r
ggplot(loans, aes(x = homeownership, fill = term)) +
geom_bar()
```
此时我们会收到一个警告,提示“在统计转换期间丢弃了以下美学属性:fill”。这是因为`ggplot2`无法推断数据正确的分组结构。它给出了一个有用的提示:“您是否忘记指定`group`美学属性或将数值变量转换为因子?”
记住,`term`虽然只有36个月和60个月两个值,但它被编码为**数值型变量**。为了创建条形图,我们需要进行一点数据转换,使用`mutate()`函数和`as.factor()`函数将其转换为因子(在R中,分类数据称为“因子”)。
```r
loans %>%
mutate(term = as.factor(term)) %>%
ggplot(aes(x = homeownership, fill = term)) +
geom_bar()
```
### 比例堆叠条形图
我们目前制作的图表存在一个挑战:不同房屋所有权水平的频率不一致(此数据集中,抵押或租赁房屋的人比自有房屋的人多),因此很难跨组比较不同期限的比例。
为了正确比较,我们需要一个显示不同期限**比例**而非频率的堆叠条形图。我们可以通过设置`geom_bar()`中的`position = "fill"`参数来实现。
```r
loans %>%
mutate(term = as.factor(term)) %>%
ggplot(aes(x = homeownership, fill = term)) +
geom_bar(position = "fill")
```
现在,抵押、自有、租赁每个条形都代表了该类别下100%的人,我们可以清楚地看到每个组内36个月期限和60个月期限贷款的比例。例如,从这个图中我们可以看到,在有抵押贷款的人群中,60个月期限贷款的比例高于其他两类。
如果我们的目标是研究房屋所有权与贷款期限之间的关系,那么应该使用这个比例图。这样,我们的结论和决策就不会受到抵押、自有或租赁房屋人数多少的影响,而是基于每个类别内部不同期限的分布情况。
### 自定义图表
现在我们有了一个满意的图表,让我们来自定义一下它。
首先,我们可以改变美学映射,让条形水平显示。这只是一个选择,无关对错。
```r
loans %>%
mutate(term = as.factor(term)) %>%
ggplot(aes(y = homeownership, fill = term)) + # 将x改为y
geom_bar(position = "fill")
```
其次,我们可以添加一些信息丰富的标签,如标题、副标题、说明数据来源的注释,以及X轴、Y轴和图例的标签。最后,我们还可以选择自己的颜色。
```r
loans %>%
mutate(term = as.factor(term)) %>%
ggplot(aes(y = homeownership, fill = term)) +
geom_bar(position = "fill") +
labs(
title = "贷款期限与房屋所有权状态",
subtitle = "按房屋所有权状态划分的36个月与60个月贷款比例",
caption = "数据来源:Lending Club",
y = "房屋所有权状态",
fill = "贷款期限(月)" # 更改图例标题
) +
scale_fill_manual(values = c("steelblue", "darkgreen")) # 自定义颜色
```
---
## 分类数据的摘要统计
我们已经可视化了数据,但这只是故事的一半。让我们也对数据进行摘要统计。
### 单变量频率表
如果我们想知道有多少人抵押、租赁或拥有自己的房屋,可以使用`count()`函数创建一个频率表。
```r
loans %>%
count(homeownership)
```
这段代码的意思是:从`loans`数据框开始,然后计算`homeownership`变量中不同水平出现的次数。结果总是一个`tibble`,其行数等于被计数变量的水平数,并且会生成一个名为`n`的新列来显示频率。我们可以看到,有4789人抵押房屋,1353人拥有房屋。
然后,我们可以使用`mutate()`在频率表中添加一个新列,用于计算**相对频率**(比例)。
```r
loans %>%
count(homeownership) %>%
mutate(proportion = n / sum(n))
```
注意,我给这个新列起了一个新名字`proportion`。我可以叫它任何名字,但使用简短且具有提示性的名称是良好的实践。
### 双变量频率与比例表
我们也可以为多个分类变量创建频率表。这里我们计算每个房屋所有权水平下,36个月和60个月贷款的数量。
```r
loans %>%
count(homeownership, term)
```
然后,我们可以使用之前用过的技巧来计算相对频率。但在使用`mutate`计算比例之前,我们做了一件新的事情:**按`homeownership`分组**。因为我希望分别计算抵押、自有、租赁各组内部36个月和60个月贷款的比例。
```r
loans %>%
count(homeownership, term) %>%
group_by(homeownership) %>%
mutate(proportion = n / sum(n))
```
这个输出显示:在抵押房屋的人群中,64.1%的人有36个月贷款,35.9%的人有60个月贷款。同样,在租赁房屋的人群中,75.2%的人有36个月贷款,24.8%的人有60个月贷款。
---

## 总结
在本节课中,我们一起学习了分类数据的可视化和摘要统计方法。我们首先区分了变量类型和分析类型。然后,使用Lending Club贷款数据集,演示了如何创建单变量条形图、双变量分段条形图以及比例堆叠条形图来可视化分类数据。最后,我们使用`count()`和`group_by()`结合`mutate()`函数,计算了单变量和双变量的频率与相对频率表,从而对数据进行了有效的数值摘要。掌握这些工具是理解数据分布和变量间关系的基础。
# 012:星球大战角色分析 🎬


在本节课中,我们将使用《星球大战》数据集进行实践,重点探索如何分析和可视化分类变量。我们将学习如何通过`ggplot2`包创建图表,并理解映射变量与设置固定属性之间的区别。
## 数据概览
首先,我们来查看将要使用的数据集。这个名为`starwars`的数据集包含87个角色(行)和14个变量(列),记录了角色的姓名、头发颜色、性别、驾驶的载具等信息。
```r
# 加载tidyverse包,它包含了ggplot2
library(tidyverse)
# 查看数据集结构
glimpse(starwars)
```
由于这个数据集随R包提供,我们可以通过帮助菜单查看其数据字典,以了解每个变量的具体含义。
## 练习1:按性别着色散点图
上一节我们查看了数据,本节中我们来看看如何创建第一个可视化图表。第一个练习要求我们修改一个散点图,根据角色的性别为数据点着色。
以下是初始代码,它绘制了角色的体重(`mass`)与身高(`height`)的关系:

```r
ggplot(data = starwars, aes(x = height, y = mass)) +
geom_point()
```

我们需要将性别(`gender`)变量映射到颜色(`color`)美学属性上。由于这是将数据框中的一列映射到图表的美学元素,因此代码应放在`aes()`函数内。
```r
ggplot(data = starwars, aes(x = height, y = mass, color = gender)) +
geom_point()
```
运行代码后,图表中的点会根据性别(女性、男性、无性别)显示为不同颜色,并且会自动生成图例说明每种颜色对应的类别。
## 练习2:将所有点设置为粉色
在上一节我们通过映射变量改变了颜色,本节中我们来看看如何设置固定的颜色属性。这个练习要求我们将所有数据点的颜色统一改为粉色。
这里的关键区别在于,我们并非根据数据变量着色,而是进行一种“装饰性”的固定设置。因此,颜色参数应直接设置在几何对象函数`geom_point()`内部。
```r
ggplot(data = starwars, aes(x = height, y = mass)) +
geom_point(color = “pink”)
```
注意,R能识别“pink”这类颜色名称,但并非所有你想到的颜色名都有效。练习二与练习三的核心区别在于:练习二是**映射**(`mapping`)颜色到变量,而练习三是**设置**(`setting`)一个固定颜色。
## 练习3:添加图表标签
现在我们已经创建了基本的图表,接下来需要为其添加清晰的标签,使图表更易读。这个练习要求我们为图表添加标题、坐标轴标签,并更新图例标题。

我们基于练习一的图表(按性别着色)进行添加。在ggplot2中,使用`+`操作符来叠加图层,`labs()`函数用于添加标签。
```r
ggplot(data = starwars, aes(x = height, y = mass, color = gender)) +
geom_point() +
labs(
title = “星球大战角色的体重与身高”,
x = “身高(厘米)”,
y = “体重(千克)”,
color = “性别”
)
```

在迭代构建可视化图表时,查阅数据文档很重要。例如,我们从帮助文档得知`height`以厘米为单位,`mass`是体重(千克)。我们根据这些信息更新了坐标轴标签。同样,要修改图例标题,需要在`labs()`函数中引用对应的美学映射名称(这里是`color`)。
## 练习4:探索单个分类变量
前面我们使用了分类变量作为辅助信息,本节我们直接聚焦于分类变量本身。这个练习要求我们选择一个分类变量,并绘制其分布的条形图。
首先,我们需要识别数据集中的分类变量。例如,`hair_color`(头发颜色)就是一个分类变量。
以下是创建条形图的步骤:
1. 使用`ggplot()`初始化,并设置`aes(x = hair_color)`。
2. 使用`geom_bar()`几何对象生成条形图。
```r
ggplot(data = starwars, aes(x = hair_color)) +
geom_bar()
```
生成的图表按字母顺序显示了所有头发颜色,条形高度表示该颜色出现的频率。我们可以看到许多角色没有头发,棕色、黑色和金色头发也很常见。
如果类别名称较长,可以将条形图翻转,让类别显示在Y轴上,这样更便于阅读:
```r
ggplot(data = starwars, aes(y = hair_color)) +
geom_bar()
```
## 练习5:探索两个分类变量之间的关系
在观察了单个变量的分布后,我们进一步探索两个分类变量之间的关系。这个练习要求我们选择两个分类变量,并可视化它们之间的关系。
我们选择`sex`(生理性别)和`gender`(社会性别角色)作为示例。一种有效的方法是使用堆叠条形图或分组条形图。
以下是创建过程:
1. 将一个变量(如`sex`)映射到X轴。
2. 将另一个变量(如`gender`)映射到填充色(`fill`)。
```r
ggplot(data = starwars, aes(x = sex, fill = gender)) +
geom_bar()
```
这个图表显示了在每个生理性别类别中,社会性别角色的分布情况。我们也可以交换变量,将`gender`放在X轴,用`sex`来填充,从而观察在每个社会性别角色中,生理性别的分布。选择哪种方式取决于你的分析重点。
## 练习6:引入第三个分类变量
当我们想同时观察三个分类变量时,二维图表可能显得拥挤。这时,可以使用分面(faceting)功能来创建“小 multiples”。
我们基于上一个图表(`sex`和`gender`),引入第三个变量`species`(物种)进行分面。使用`facet_wrap()`函数,按物种将图表分割成多个小面板。
```r
ggplot(data = starwars, aes(x = sex, fill = gender)) +
geom_bar() +
facet_wrap(~species)
```
分面后,我们可以分别观察每个物种内部,角色的生理性别与社会性别角色的关系。需要注意的是,当样本量较小(如本数据集只有87个角色)而引入过多变量时,图表可能变得难以解读或信息量有限。探索过程中遇到死胡同是正常的,你可以随时回溯并尝试其他分析路径。
## 总结
本节课中我们一起学习了如何使用`ggplot2`对《星球大战》数据集中的分类变量进行可视化分析。我们实践了:
1. 在散点图中通过`aes()`**映射**分类变量来着色。
2. 在几何对象函数中直接**设置**固定颜色属性。
3. 使用`labs()`函数为图表添加标题、坐标轴标签和图例标题。
4. 使用`geom_bar()`创建单个分类变量的分布条形图。
5. 使用堆叠条形图探索两个分类变量之间的关系。
6. 使用`facet_wrap()`进行分面,以同时观察三个分类变量。

记住,优秀的数据可视化往往是一个迭代过程,不要期望第一次就得到完美结果。不断尝试、修改和查阅文档,是数据科学探索中的关键步骤。
# 013:数值数据的可视化与摘要统计

## 概述
在本节课中,我们将学习如何对数值型数据进行可视化呈现和摘要统计。我们将介绍描述数据分布的关键特征,并学习使用R语言中的`ggplot2`等工具来创建直方图、密度图和箱线图,以及如何计算均值、中位数、分位数等摘要统计量。
---
## 数据分布的特征
在讨论数值变量的分布时,我们通常关注四个关键特征:形状、中心、离散程度和异常值。
* **形状**:指分布的形态。我们关注其**模态**,即它是单峰的(有一个明显的峰)、双峰的(有两个明显的峰)还是多峰的。
* **中心**:指分布的中心位置。对于对称分布,通常使用**均值**来描述中心;对于偏态分布,特别是严重偏态的分布,**中位数**是更好的中心度量,因为它对极端值不敏感。
* **离散程度**:指数据的分散情况。对于对称分布,我们使用**标准差**;对于偏态分布,我们使用**四分位距**,它给出了中间50%数据的范围。
* **异常值**:指明显偏离其他数据点的观测值。我们需要指出它们是否存在,以及位于分布的高端还是低端。最好能识别并探究这些异常值出现的原因。
上一节我们介绍了描述数据分布的理论框架,本节中我们来看看如何通过可视化来观察这些特征。
---
## 数值数据的可视化
### 直方图
直方图有助于识别分布的形状,让我们能够观察模态和偏态。创建直方图需要仔细选择**条柱宽度**。
以下是创建直方图的步骤。首先,我们使用`ggplot()`函数并指定数据框和映射到x轴的变量。
```r
ggplot(loans, aes(x = loan_amount))
```
然后,我们添加`geom_histogram()`几何对象来绘制直方图。默认情况下,`ggplot2`会使用30个条柱,但通常会建议我们手动设置更合适的`binwidth`参数。
```r
ggplot(loans, aes(x = loan_amount)) +
geom_histogram(binwidth = 5000)
```


选择条柱宽度是一门艺术也是一门科学。宽度太小会导致图形过于“尖刺”,宽度太大则会丢失细节。需要根据数据背景和可视化目标进行尝试,直到找到一个能清晰展示分布形态的宽度。
创建直方图后,我们通常需要对其进行一些自定义以增强可读性。
```r
ggplot(loans, aes(x = loan_amount)) +
geom_histogram(binwidth = 5000) +
labs(
title = "Distribution of Loan Amounts",
x = "Loan Amount (USD)",
y = "Count"
) +
scale_x_continuous(labels = label_dollar()) +
scale_y_continuous(labels = label_number(big.mark = ","))
```
在上面的代码中,我们使用`labs()`函数添加了标题和坐标轴标签。通过`scales`包中的`label_dollar()`和`label_number()`函数,我们可以让坐标轴标签的格式更美观(例如,为金额添加美元符号和千位分隔符)。
### 密度图
密度图是另一种可视化数值数据分布形状(模态和偏态)的方法。与直方图类似,你可以通过`adjust`参数控制曲线的平滑程度。
```r
ggplot(loans, aes(x = loan_amount)) +
geom_density(adjust = 0.5)
```
`adjust`参数的值越小,密度曲线越紧密地跟随数据的细节(可能更“崎岖”);值越大,曲线越平滑。同样,需要尝试不同的值,以找到在数据背景下最有意义的可视化效果。
### 箱线图
箱线图对于识别数据的特定标记点非常有用,包括最小值、最大值、第25百分位数(箱体底部)、中位数(箱体内的粗线)和第75百分位数(箱体顶部)。
此外,箱线图也有助于识别分布的偏态。**须线**较长的一侧就是分布偏斜的方向。然而,箱线图无法展示分布的模态(即“峰”的数量)。
箱线图的一个显著优点是,它能根据严格的异常值定义,清晰地显示出异常值。
```r
# 绘制贷款金额的箱线图
ggplot(loans, aes(y = loan_amount)) +
geom_boxplot()
# 绘制利率的箱线图,可以看到高端有一些异常值点
ggplot(loans, aes(y = interest_rate)) +
geom_boxplot()
```
要可视化不同的变量,只需更改美学映射中的变量名即可。
我们也可以像自定义直方图一样自定义箱线图。
```r
ggplot(loans, aes(y = interest_rate)) +
geom_boxplot() +
labs(
title = "Distribution of Interest Rates",
x = "" # 箱线图的x轴标签通常无信息,可留空
) +
theme(
axis.text.y = element_blank(), # 移除y轴刻度文本
axis.ticks.y = element_blank() # 移除y轴刻度线
)
```
在箱线图中,y轴的刻度位置和范围并不传达关于数据分布的具体信息(箱体宽度和位置才是关键),因此有时可以将其隐藏以使图形更简洁。
我们已经探讨了如何可视化数值数据,现在让我们学习如何用数字来概括它。
---
## 数值数据的摘要统计
### 使用`summarize`函数
`summarize`函数用于计算数据中所有观测值的摘要统计量。
以下代码计算数据集中贷款金额的均值。
```r
loans %>%
summarize(mean_loan_amount = mean(loan_amount))
```
你可以使用`summarize`函数一次性计算多个摘要统计量。
```r
loans %>%
summarize(
q25 = quantile(loan_amount, 0.25), # 第25百分位数
q75 = quantile(loan_amount, 0.75) # 第75百分位数
)
```
这段代码计算了第25和第75百分位数,它们定义了箱线图中箱体的上下边界,也就是中间50%数据的范围。
### 为统计量命名的最佳实践
虽然`summarize`函数在不命名或使用非信息性名称时也能工作,但不推荐这样做。
**不推荐的写法:**
```r
# 名称无意义
loans %>% summarize(temp = mean(loan_amount))
# 不命名,列标题会成为复杂的表达式
loans %>% summarize(mean(loan_amount))
```
**推荐的写法:**
```r
loans %>% summarize(mean_loan_amt = mean(loan_amount))
```
为摘要统计量起一个简短且具有描述性的名称是很好的实践。这能让你在日后回顾代码时,立即明白每个数字代表什么,从而提高代码的可读性和可维护性。
---

## 总结
本节课中,我们一起学习了数值数据的可视化与摘要统计。我们首先了解了描述数据分布的四个关键特征:形状、中心、离散程度和异常值。接着,我们实践了如何使用直方图、密度图和箱线图来可视化这些特征,并学习了如何通过`ggplot2`和`scales`包来创建和美化这些图形。最后,我们掌握了使用`summarize`函数计算均值、分位数等摘要统计量的方法,并强调了为统计结果起一个清晰名称的重要性。掌握这些技能是进行数据探索和分析的基础。
# 014:变量间关系的可视化与摘要统计

在本节课中,我们将学习探索变量之间关系的工具与技术。我们将涵盖如何可视化及量化两个数值变量、一个数值与一个分类变量,以及两个分类变量之间的关系。最后,我们将探讨如何同时分析多个变量之间的关系。
## 设置分析环境
我们将使用 `tidyverse` 包进行数据处理与可视化,使用 `openintro` 包获取数据,并使用 `scales` 包优化坐标轴标签。
以下是加载所需包的代码:
```r
library(tidyverse)
library(openintro)
library(scales)
```
我们将使用的数据来自北卡罗来纳州达勒姆市的杜克森林社区。该数据集包含了2020年11月从Zillow网站抓取的房屋价格及其他属性信息,名为 `duke_forest`。
查看数据概览,可以看到它有98个观测值和13个变量。
## 两个数值变量间的关系
首先,我们探讨两个数值变量间的关系。例如,我们预期房屋价格与面积之间应存在正相关关系:面积越大,价格越高。
### 可视化:散点图
散点图是可视化两个数值变量关系的绝佳起点。我们可以通过将X和Y美学映射到数据中的变量,并使用 `geom_point()` 函数来创建散点图。
### 量化:相关系数
我们可以使用相关系数来量化这种关系。相关系数是一个介于-1到1之间的值,其中-1表示完全负相关,1表示完全正相关,0表示无线性相关。
计算相关系数的R函数是 `cor()`。在 `summarize` 语句中计算如下:
```r
cor(price, area)
```
在本例中,计算出的相关系数为0.667,表明存在中等强度的正相关关系。
### 评估关系形式
在计算相关系数之前,我们必须评估关系的**形式**,特别是它是否为线性。相关系数衡量的是两个数值变量之间的**线性**关联。如果关联不是线性的,则不应首先计算相关系数。幸运的是,本例中的关联看起来是线性的。
## 数值变量与分类变量间的关系
接下来,我们看看如何分析一个数值变量(如价格)与一个分类变量(如冷却类型)之间的关系。
### 可视化:分组直方图与密度图
一种方法是在直方图中,通过 `fill` 美学将条形按分类变量着色。然而,这种图形可能难以解读。
更好的替代方案是并排箱线图。通过将Y美学映射到分类变量,我们可以轻松比较不同组的中位数、四分位距和异常值。
填充密度图也很有帮助,尤其适用于比较不同组之间的分布形状。创建此图时,需要将 `color` 和 `fill` 美学都映射到分类变量,并设置 `alpha` 值以确保两条曲线都清晰可见。
另一种有用的表示形式是小提琴图,它结合了箱线图和密度图的特点,可以使用 `geom_violin()` 创建。
### 量化:分组摘要统计
除了可视化,我们还应该定量地总结这种关系。我们可以按分类变量对数据进行分组,然后计算每个组的摘要统计量。
以下是计算每个冷却类型组中房屋价格中位数的代码示例:
```r
duke_forest %>%
group_by(cooling) %>%
summarize(median_price = median(price))
```
我们还可以一次性计算多个统计量,例如最小值、最大值、中位数和四分位距,这有助于构建描述价格与冷却类型之间关系的叙述。
## 两个分类变量间的关系
现在,我们来探讨两个分类变量之间的关系。数据集中没有合适的第二个分类变量,因此我们先创建一个。
### 创建新分类变量
我们将基于房屋的建造年份创建一个名为 `year_built_cat` 的新变量。如果房屋建于1970年或之后,则标记为“1970 or later”;否则标记为“1969 or earlier”。
以下是使用 `mutate()` 和 `ifelse()` 函数创建此变量的代码:
```r
duke_forest <- duke_forest %>%
mutate(
year_built_cat = ifelse(
year_built >= 1970,
"1970 or later",
"1969 or earlier"
)
)
```
创建后,建议检查新变量是否正确生成。
### 可视化:堆叠条形图
我们可以制作堆叠条形图来显示新旧房屋中冷却类型的分布。由于新房屋数量较少,查看比例可能更有意义,这可以通过在 `geom_bar()` 中设置 `position = "fill"` 来实现。
### 量化:计数与比例
我们可以使用 `count()` 函数计算落入每个类别的房屋数量(频率)。然后,通过分组和计算 `n / sum(n)` 来得到每个组内的比例。
分析显示,对于1970年或之后建造的新房屋,拥有中央空调的比例更高;而较老的房屋则更可能使用其他冷却设置。
## 多个变量间的关系
最后,我们来探讨同时可视化与总结多个变量关系的更高级方法。
### 多变量散点图
我们可以创建一个散点图,其中X轴为面积,Y轴为价格,并通过点的颜色和形状来区分不同的冷却类型。这能让我们在一个图中同时观察多个关系。
### 分组计算相关系数
鉴于关系看起来是线性的,我们可以为每个冷却组分别计算价格与面积之间的相关系数。这通过 `group_by()` 后接 `summarize()` 来实现。
分析结果可能出人意料:中央空调组的相关系数(0.854)远高于其他冷却类型组(0.459)。这表明对于有中央空调的房屋,面积与价格之间的线性关系更强。

### 避免过度绘制:分面图
散点图中的点可能重叠严重,使得模式难以看清。使用分面是避免过度绘制的有效方法。我们可以使用 `facet_wrap(~ cooling)` 按冷却类型将图分成多个面板。

分面后,颜色和图例变得冗余,可以通过在相应的 `geom`(如 `geom_point`)中设置 `show.legend = FALSE` 来关闭图例,从而节省空间。
## 总结
在本节课中,我们一起学习了探索变量间关系的多种方法:
1. 对于两个**数值变量**,我们使用**散点图**进行可视化,并使用**相关系数**进行量化,同时评估了关系的线性形式。
2. 对于**数值与分类变量**,我们使用了**并排箱线图**、**密度图**和**小提琴图**进行可视化,并通过**分组摘要统计**(如中位数)进行量化。
3. 对于两个**分类变量**,我们创建了**堆叠条形图**来显示分布,并计算了**频率和比例**。
4. 对于**多个变量**,我们通过**多美学映射散点图**和**分面图**来同时探索复杂关系,并进行了**分组相关性分析**。

通过结合这些可视化与定量工具,我们可以更全面、深入地理解数据中变量间的相互作用,为后续的数据分析与建模打下坚实基础。
# 015:深入探索帕尔默企鹅数据集 🐧


在本节课中,我们将深入探索帕尔默企鹅数据集。我们将学习如何使用R语言和`tidyverse`包进行数据整理与可视化,并引入`ggbeeswarm`扩展包来绘制新的图形。我们将通过回答诸如“企鹅体型在不同物种、岛屿和性别间有何差异?”等问题,来实践单变量、双变量及多变量分析。
## 概述
我们之前接触过帕尔默企鹅数据集,但仅停留在表面。本次代码实践将深入探索`penguins`数据框,分析企鹅体型(如体重)如何随物种、岛屿和性别变化,以及其他特征。我们将使用`tidyverse`包进行数据处理和可视化,并引入`ggbeeswarm`包作为`ggplot2`扩展包的例子,它提供了一个`ggplot2`本身未包含的新几何对象(geom)。
首先,让我们加载所需的包和数据。
```r
# 加载必要的包
library(tidyverse)
library(ggbeeswarm)
library(palmerpenguins)
# 查看数据框结构
glimpse(penguins)
```
数据框包含344只企鹅的观测记录,共有8列变量信息。
## 单变量分析:企鹅体重
单变量分析指对单个变量进行分析。我们首先可视化企鹅体重的分布。请注意,体重记录单位为克(grams)。
### 绘制直方图


我们使用`ggplot2`绘制体重的直方图。
```r
# 绘制体重直方图
ggplot(data = penguins, aes(x = body_mass_g)) +
geom_histogram()
```

运行代码后,我们会得到一个直方图,并看到两条警告信息。一条提示移除了2行包含缺失值(NA)的数据;另一条提示默认使用了`bins = 30`来划分数据区间,并建议我们手动选择更合适的区间宽度(带宽)。



选择带宽是科学与艺术的结合。我们可以尝试不同的值,例如1000、10、100或150,观察图形变化,并选择在数据背景下有意义的、易于解释的整数值(如100)。
```r
# 尝试不同带宽的直方图
ggplot(data = penguins, aes(x = body_mass_g)) +
geom_histogram(binwidth = 100)
```
从直方图可以看出,体重分布呈右偏态,典型体重约在3500克左右,右侧(较高体重)的尾部比左侧更长。
### 绘制箱线图

接下来,我们绘制同一变量(体重)的箱线图。

```r
# 绘制体重箱线图
ggplot(data = penguins, aes(x = body_mass_g)) +
geom_boxplot()
```

箱线图能更清晰地显示中位数(约4000克)、四分位距(箱体的宽度)以及数据的范围。图中未显示异常值。
### 判断分布形态

基于直方图和箱线图,判断以下关于分布形态的陈述是否正确:
1. **样本中企鹅体重的分布是左偏的。**
**答案:错误。** 从直方图可见,分布有一个更长的右侧尾部,属于右偏分布。
2. **样本中企鹅体重的分布是单峰的。**
**答案:难以确定。** 仅从直方图判断峰态(modality)较为困难。调整带宽后,图形可能显示一个主峰(约3500克)和另一个潜在的小峰。为了进一步探究,我们可以尝试密度图。
```r
# 绘制体重密度图
ggplot(data = penguins, aes(x = body_mass_g)) +
geom_density()
```
密度图可能显示存在不止一个峰值,这提示我们可能需要引入另一个变量进行深入分析。
## 双变量分析:体重与物种
双变量分析探索两个变量之间的关系。现在我们分析体重如何随物种变化。

### 分面直方图

首先,我们创建一个包含物种信息的直方图,并使用`fill`美学映射为不同物种着色。

```r
# 按物种填充颜色的直方图
ggplot(data = penguins, aes(x = body_mass_g, fill = species)) +
geom_histogram(binwidth = 100)
```

由于条形重叠,难以看清。我们可以使用`facet_wrap`将不同物种的直方图分开显示,并排成一列以便比较。
```r
# 按物种分面的直方图
ggplot(data = penguins, aes(x = body_mass_g)) +
geom_histogram(binwidth = 100) +
facet_wrap(~ species, ncol = 1)
```
从图中可以看出,阿德利企鹅(Adelie)和帽带企鹅(Chinstrap)的体重峰值接近,而金图企鹅(Gentoo)的峰值明显更高,平均体型更大。这或许解释了之前单变量密度图中看到的潜在多峰现象。
### 并排箱线图
另一种可视化数值变量(体重)与分类变量(物种)关系的方法是并排箱线图。
```r
# 按物种分组的箱线图
ggplot(data = penguins, aes(x = body_mass_g, y = species)) +
geom_boxplot()
```
箱线图清晰地显示了金图企鹅的中位数体重远高于其他两种。我们还可以添加颜色并移除冗余的图例。
```r
# 添加颜色并隐藏图例的箱线图
ggplot(data = penguins, aes(x = body_mass_g, y = species, color = species)) +
geom_boxplot(show.legend = FALSE)
```
### 密度曲线图
我们可以绘制按物种填充的密度曲线图。为了避免曲线相互遮挡,可以设置透明度(alpha)。
```r
# 按物种着色的密度曲线图(带透明度)
ggplot(data = penguins, aes(x = body_mass_g, fill = species)) +
geom_density(alpha = 0.5)
```
### 小提琴图
小提琴图是另一种展示数值与分类变量关系的图形,它结合了箱线图和密度图的信息。
```r
# 小提琴图
ggplot(data = penguins, aes(x = body_mass_g, y = species)) +
geom_violin()
```
我们可以进一步添加颜色和填充,并调整透明度。
```r
# 添加美学映射的小提琴图
ggplot(data = penguins, aes(x = body_mass_g, y = species)) +
geom_violin(aes(color = species, fill = species), alpha = 0.5, show.legend = FALSE)
```
### 抖动散点图与蜂群图
当数据点大量重叠时,可以使用抖动散点图或蜂群图。
**抖动散点图**:通过添加随机噪声来避免点重叠。为了确保结果可重现,可以设置随机种子。
```r
# 设置随机种子以保证可重现性
set.seed(123)
# 抖动散点图
ggplot(data = penguins, aes(x = species, y = body_mass_g)) +
geom_jitter(width = 0.2, alpha = 0.5)
```
**蜂群图**:使用`ggbeeswarm`包的`geom_beeswarm`,它能在高密度区域更智能地排列点,形似蜂群。
```r
# 蜂群图
ggplot(data = penguins, aes(x = species, y = body_mass_g)) +
geom_beeswarm(aes(color = species), show.legend = FALSE)
```
## 多图层与多变量分析
我们可以在一个图形上叠加多个几何对象(geoms)。需要注意图层的绘制顺序。
### 组合多个几何对象
例如,我们可以将小提琴图、蜂群图和箱线图组合在一起。为了避免全局美学映射干扰,我们将其定义在各自的几何对象层中。
```r
# 组合小提琴图、蜂群图和箱线图
ggplot(data = penguins, aes(x = body_mass_g, y = species)) +
geom_violin(aes(color = species, fill = species), alpha = 0.2, show.legend = FALSE) +
geom_boxplot(width = 0.1, show.legend = FALSE) +
geom_beeswarm(aes(color = species), size = 0.8, show.legend = FALSE)
```
在迭代构建图形时,应定期检查并清理那些不再需要或无效的代码。
### 按岛屿分面
现在,我们引入第三个变量:岛屿。将上一节创建的图形按岛屿进行分面。
```r
# 按岛屿分面,并添加标签
ggplot(data = penguins, aes(x = body_mass_g, y = species)) +
geom_violin(aes(color = species, fill = species), alpha = 0.2, show.legend = FALSE) +
geom_beeswarm(aes(color = species), size = 0.8, show.legend = FALSE) +
facet_wrap(~ island, ncol = 1) +
labs(
x = "体重 (克)",
y = "物种",
title = "不同物种和岛屿上企鹅的体重分布"
)
```
分面图显示,不同岛屿上的物种组成不同(例如,Bisco岛有金图企鹅和阿德利企鹅,而Dream岛有帽带企鹅和阿德利企鹅)。
## 图形输出与代码块选项
渲染文档时,我们可能希望控制图形大小并隐藏重复的警告信息。
* **全局设置**:在文档顶部的代码块选项中设置,可应用于所有图形。
```r
# 在文档开头的代码块选项中设置
knitr::opts_chunk$set(
warning = FALSE, # 隐藏警告
fig.width = 6, # 图形宽度
fig.asp = 0.618 # 图形宽高比(黄金比例)
)
```
* **局部设置**:在单个代码块中设置,仅对该图形生效。
```r
# 在特定代码块中设置
# {r, fig.asp=0.3}
ggplot(...) + ...
```
## 总结

本节课中,我们一起深入探索了帕尔默企鹅数据集。我们从单变量分析(体重分布)开始,学习了直方图、箱线图和密度图的绘制与解读。接着,我们进行了双变量分析,探索了体重与物种的关系,并实践了分面直方图、并排箱线图、小提琴图、抖动散点图和蜂群图等多种可视化方法。最后,我们引入了第三个变量(岛屿),通过分面和组合多个几何对象进行了多变量分析,并学习了如何控制图形输出与处理警告信息。这些技能将帮助你更有效地探索和呈现数据中的模式与关系。
# 016:Git与GitHub 🛠️

在本课程中,我们将学习版本控制的概念及其在数据科学工作流中的重要性。我们将重点介绍如何使用Git和GitHub来管理项目,确保代码的追踪、协作与安全。
## 什么是版本控制?
版本控制是追踪和管理文件变更的过程,它允许你查看自己或他人所做的修改。有效的版本控制能避免项目陷入混乱。
上一节我们介绍了版本控制的基本概念,本节中我们来看看一个反面例子。以下是一种低效的版本管理方式:
* 一位研究生在撰写最终文档。
* 导师发回带有修改意见的版本。
* 经过多次来回修改后,文件夹里堆满了诸如“最终版”、“最最终版”、“真的最终版”之类的文件,以及难以追踪的评论和建议。
这种方式效率低下且容易出错。
## 优秀的版本控制:像搭乐高房子 🧱
优秀的版本控制就像搭建一座乐高房子。每一步操作都被清晰地记录和追踪,整个过程易于追溯。
如果你需要快速查看他人对文件的修改,或者将文件回退到之前的某个步骤,有条理的组织方式会让这一切变得非常简单。
如果每一步都有简短、可读的信息来描述完成了什么工作,这个过程会变得更好、更易于追踪。例如,第二步的信息可能是“搭建了房屋底座的前后部分”。这对于你的协作者或日后回顾项目的你自己都非常有用。
在使用版本控制时,你可以自行决定何时创建这些记录信息。这可能是在写完一个段落之后,也可能是在完成一个可视化图表之后。
## 版本控制工具:Git与GitHub
现在,我们来看看实现优秀版本控制的软件。Git是一款流行的版本控制软件,尤其在统计和数据科学社区。你可以将其类比为Word中的“跟踪更改”功能,但它提供了更多对用户有帮助的功能。
我们还将使用GitHub。GitHub是一个基于云的Git项目托管服务。它类似于Dropbox,但同样提供了更多有用的功能。
你的课程项目将存放在GitHub上。
## GitHub与RStudio实战演练 🚀
接下来,我们将进行一次实战演练,学习如何生成并配置个人访问令牌,以便RStudio与GitHub通信;如何克隆你的项目;以及如何在处理文档时使用版本控制。
以下是配置Git和RStudio的步骤:
1. **安装`usethis`包**:首先,在RStudio的控制台中运行以下代码来安装必要的包。
```r
install.packages("usethis")
```
2. **配置Git用户信息**:接着,使用`usethis`包中的函数设置你的姓名和邮箱。请确保将示例文本替换为你自己的真实信息。
```r
library(usethis)
use_git_config(user.name = "你的姓名", user.email = "你的邮箱")
```
### 创建个人访问令牌
现在,我们需要创建个人访问令牌,以便GitHub和RStudio能够相互通信,让我们能在本地的RStudio中处理所有文件。
我们将使用`usethis`包中的`create_github_token()`函数。在控制台运行此代码后,它会弹出一个网站页面让你创建令牌。
在创建令牌时,你需要:
* **描述用途**:例如“项目演示”。
* **设置有效期**:GitHub通常建议设置一个有效期(如30、60、90天),而不是“永不过期”。你可以将此令牌视为允许两个程序相互通信的密码。
* **选择权限范围**:GitHub会为你推荐一些权限范围,这些范围足以满足项目需求,例如克隆仓库、推送代码等。
完成上述设置后,点击“生成令牌”。这个令牌对每个人都是不同的。点击复制图标,复制此令牌。
### 在RStudio中配置令牌
复制令牌后,我们需要在RStudio中配置它。回到RStudio控制台,使用`gitcreds`包中的`gitcreds_set()`函数。
具体操作如下:
```r
library(gitcreds)
gitcreds_set()
```
运行后,控制台会提示你输入密码或令牌。将刚才复制的令牌粘贴进去,按回车键。你会看到RStudio添加凭证并移除任何可能存在的旧凭证的提示。当看到“完成”时,就表示配置成功。现在,RStudio和GitHub已配置好,你可以从GitHub克隆仓库到本地计算机,并在RStudio中处理这些文件了。
### 克隆你的项目
既然我们已经具备了访问权限,接下来演示如何克隆项目。
回到你拥有的个人项目仓库页面,点击绿色的“Code”按钮,选择“HTTPS”,然后复制显示的URL。
接着,在RStudio中创建一个新项目。你可以将新项目视为从GitHub克隆到本地的个人项目文件夹。我们点击“File” -> “New Project…” -> “Version Control” -> “Git”。点击“Git”后,系统会提示你粘贴刚才复制的仓库URL。同时,你需要设置项目目录名称,并选择在本地计算机上的保存位置。
建议创建一个专门的文件夹(例如“repos”)来保存你在这个专项课程中克隆的所有仓库。然后,点击“Create Project”。
创建项目后,所有最初在GitHub仓库中的文件都会出现在这里。你可以点击任何一个文件,在RStudio窗口中与之交互。
## Git版本控制工作流演示 🔄
现在我们已经克隆了项目,我想演示如何在此环境中使用Git进行版本控制。
首先,请注意环境窗口上方有一个“Git”选项卡,里面有“Commit”、“Pull”和“Push”按钮。
打开一个新项目时,我们通常要做的第一件事是“Pull”(拉取)。虽然首次克隆项目时这不是必须的,但养成每次打开项目都先拉取的习惯非常好。因为拉取能确保你本地要处理的所有文件,与远程仓库中的文件完全一致。这一点在团队协作中尤为重要。
确认文件已是最新后,我们就可以开始在这些文档中进行修改和工作了。
我要做的第一个修改是更改作者名为我的名字。然后,保存这个文档。
在我做出修改并保存后,在Git面板中可以看到`index.qmd`文件现在出现了。假设我想保存这个更改。
我们可以把“Commit”(提交)按钮想象成一个保存按钮,或者认为它是我们想在乐高房子上添加另一块积木并附上适当信息的地方。
在这个提交窗口中,我可以看到`index.qmd`文件。它会用红线显示我删除了什么,用绿线显示我添加了什么。如果认可这些更改,我就将它们“Stage”(暂存),这相当于准备将它们推送到GitHub仓库。
然后,我添加一条有用的提交信息,例如“添加了我的作者名”,接着点击“Commit”保存或提交它。
现在,我们看到一条消息:“Your branch is ahead of ‘origin/main’ by 1 commit”,意思是我们本地的内容与我们克隆的远程仓库内容不同了。
接下来,我们点击“Push”(推送)将这些更改推送到远程。稍等片刻,那条消息就会消失,`index.qmd`文件也不再显示在这里。如果我们回到GitHub上的仓库页面并刷新,就能看到我们添加的有用提交信息,并且能看到`index.qmd`文件刚刚被更改了。这很棒!
这就是良好的版本控制的一般工作流程。我们鼓励你在整个项目过程中使用并实践良好的版本控制。
## 使用GitHub Pages展示项目 🌐
我想展示的另一点,也是我们使用GitHub的另一个原因,是希望为你提供机会,将这些项目作为工作成果的证明,并链接到你的简历上,通过一个实时URL在线展示。
要进行此设置,你需要再次进入你拥有的个人仓库页面。点击“Settings”(设置),然后找到“Pages”(页面)选项。
在“Build and deployment”(构建和部署)部分,选择从“main”分支部署,然后点击“Save”(保存)。这将把你的项目和所有辛勤工作托管到一个实时URL上,供你展示。
点击保存后,我们可以返回仓库主页。要获取那个URL,可以点击页面右上角仓库描述旁边的齿轮图标(Settings),在“GitHub Pages”部分,你会看到系统自动生成的网站URL。点击该链接即可访问。
请注意,这个URL不会立即生效。你可以点击仓库顶部的“Actions”(操作)选项卡,看到它正在构建你的网站。这可能需要一到两分钟。构建完成后,你就可以点击那个链接,看到你的项目网站了。
## 总结与回顾 📝
本节课中我们一起学习了版本控制的核心概念与Git、GitHub的基本操作。
首先,你需要考虑创建一个GitHub账户。建议使用你的真实姓名,选择一个经得起时间考验且你愿意展示给未来老板看的用户名。
接下来,确保你拥有项目仓库的所有权。点击“Use this template”(使用此模板)创建新仓库,并确认你是所有者。建议将仓库命名为与原始仓库相同的名称。
然后,在RStudio控制台中运行指定的两行代码来生成和配置你的个人访问令牌。记住,`create_github_token()`生成令牌供你复制,`gitcreds_set()`提示你粘贴令牌以使RStudio和GitHub能够通信。
一切配置完成后,我们需要克隆仓库。操作是:在GitHub仓库页面点击“Code” -> “HTTPS”,复制URL;然后在RStudio中点击“File” -> “New Project…” -> “Version Control” -> “Git”,粘贴URL以克隆仓库。
克隆项目并获得可工作的文档后,你就可以练习使用正确的版本控制工作流程了。再次建议,在开始处理项目前先进行“Pull”操作,这在多人协作项目中尤为重要,因为它能确保你的文档是最新的,并且与仓库中的内容完全匹配。
当你工作到一个合适的节点想要保存时,点击“Commit”,暂存你的更改,并编写简短、信息丰富的提交信息,然后将这些更改“Push”到你的GitHub仓库。
所有这些Git命令都可以在RStudio环境窗口的“Git”选项卡下找到。点击Git选项卡会显示你可以进行推送、提交和拉取操作的地方。


有许多Git命令可以帮助我们进行版本控制,但很少有人全部掌握,通常只坚持使用拉取、提交和推送。如果你需要更多关于在R中使用Git的资源,我鼓励你继续阅读《Happy Git and GitHub for the useR》。
**R语言数据科学:课程2:数据整理与导入**

**概述**
在本课程中,我们将继续探索数据科学流程,重点学习数据整理、数据导入以及从网络获取数据的方法。这是系列课程的第二门,旨在帮助你将原始数据转化为可用于分析的整洁格式。
上一节我们介绍了数据科学循环的整体框架。本节中,我们来看看本课程的具体学习目标。
**课程内容**
本课程将延续对数据转换的探索,正式介绍**数据整理**。数据整理通常涉及重塑和连接数据。随后,我们将进一步回溯,学习如何导入数据集,并讨论将数据导入R时需考虑的诸多因素和挑战。
我们将从处理矩形数据(如CSV文件和Excel电子表格)开始,然后进一步学习如何通过**网络爬虫**从网络上获取非结构化数据。
**预备知识**
既然这是系列课程的第二门,你需要具备哪些预备知识呢?即使你已经完成了第一门课程“数据可视化与转换”,也建议你回顾以下内容。
以下是进入本课程前应掌握的基本工具和概念:
* **R与RStudio**:对R语言和RStudio集成开发环境有基本了解。
* **Quarto**:具备使用Quarto编写简单可重复文档的经验。
* **Tidyverse工具**:拥有使用Tidyverse工具(主要是`ggplot2`和`dplyr`)进行数据可视化和转换的经验。
Tidyverse功能强大,可应对众多数据科学任务,其中许多超出了本课程范围。目前,只要你了解以下几点,就应该可以顺利学习:
* 大多数Tidyverse函数将**数据框**作为其第一个参数。
* 可以使用`dplyr`在**数据管道**中进行数据转换。
* 可以使用`ggplot2`**逐层构建**数据可视化。
如果你需要对这些内容进行入门学习或复习,建议先完成第一门课程或阅读推荐材料。

**总结**

本节课我们一起学习了本课程“数据整理与导入”的概述、核心内容以及所需的预备知识。如果你已准备就绪,让我们开始深入学习吧。
# 018:整洁数据 📊


在本节课中,我们将要学习“整洁数据”的核心概念。我们将了解整洁数据的定义、特征,并通过实例对比整洁与不整洁的数据,最后学习如何将数据整理成适合分析的形式。
---
## 概述
我们一直在讨论“整洁宇宙”(tidyverse),现在是时候明确定义什么是“整洁数据”了。整洁数据遵循特定的结构原则,这使得数据分析和可视化过程更加高效和一致。相反,不整洁的数据则可能存在多种多样的结构问题。一个数据集对一种分析是整洁的,对另一种分析则可能不够理想。因此,我们需要学习如何整理和重塑数据。
## 整洁数据的定义
为了阐明概念,我们可以引用列夫·托尔斯泰的一句话来开场:“幸福的家庭都是相似的,不幸的家庭各有各的不幸。”这与整洁数据有何关联?虽然并非所有整洁数据都完全相同,但我们可以定义其共同特征。
整洁数据遵循以下三个核心原则:
1. **每个变量构成一列**。
2. **每个观测构成一行**。
3. **每种观测单元类型构成一个表格**。
另一方面,就像不幸的家庭一样,不整洁的数据可以以多种方式呈现混乱。此外,一个对某种分析是整洁的数据集,对另一种分析可能并非最理想的结构。
## 不整洁数据的示例
以下是几个不整洁数据的例子,让我们看看它们具体违反了哪些原则。
### 示例一:历史数据集
这个数据集来自一位记录一战飞机信息的爱好者。它显示了每个月按类型划分的可用飞机数量。
**问题所在**:虽然大多数行代表月份,但有些行代表年份,还有些行是合并单元格,用于指示历史上的某些重要时间点。
**整理方法**:为了整理这些数据,我们需要添加一个名为 `year` 的列。我们甚至可以再添加一列来记录重要事件,并在相应的月份中标注。
### 示例二:Gapminder数据集
这个数据集来自Gapminder网站,该网站维护了大量国家层面的信息。此特定数据显示了各国各年份15至49岁人群的估计HIV患病率。
**问题所在**:橙色高亮的单元格描述的是数据集本身,而不是命名该列的值。
**整理方法**:为了使这个数据集整洁,该单元格应改为类似“国家”或“国家名称”的内容。
### 示例三:美国社区调查数据
这些数据是在个人层面收集的,但由于隐私问题,美国和许多其他国家以汇总形式发布此类信息。
**问题所在**:这是一个组织良好的表格,但本质上是一个汇总统计表。虽然表格由行和列组成,但其内容并不遵循整洁数据原则。每一行代表一个子组,有些子组包含其他子组;例如,“民用劳动力”数量包含了“就业”和“失业”数量。
## 汇总统计表与原始数据
为了澄清“汇总统计表”的含义,我们可以参考一个你可能见过的数据集:关于《星球大战》角色的数据,其中每一行代表一个角色,每一列是该角色的一个属性。
例如,在第一行,我们可以看到卢克·天行者的身高和性别。
* 在左侧的表格中,我们可以看到每个性别类别中有多少角色(在名为 `N` 的列中)以及他们的平均身高(在名为 `average_height` 或 `avg_height` 的列中)。这些汇总统计是通过 `group_by()` 和 `summarize()` 语句获得的。
* 右侧的表格只是简单地显示原始数据。
对于大多数分析,我们希望从右侧的原始数据表开始,而不是已经汇总好的数据。虽然这两个数据框都可以被认为是整洁的,但原始数据表提供了更细粒度的信息,并为基于此信息进行更多样化的分析敞开了大门。
---
## 总结

本节课中,我们一起学习了整洁数据的基本原则:每个变量一列、每个观测一行、每种观测单元一个表。我们通过多个实例识别了不整洁数据的常见问题,例如混合层级的数据、不当的列名以及汇总统计表与原始数据的区别。理解并实践这些原则是进行有效数据分析和利用tidyverse工具包的基础。
R语言数据科学:03_01_02:数据整理

在本节课中,我们将要学习数据整理的核心概念,特别是如何通过“数据透视”来重塑数据,使其从“宽”格式转换为“长”格式,或反之。我们将使用 `tidyverse` 包中的工具来完成这些操作。

我们首先定义了什么是整洁的数据。接下来,让我们谈谈如何整理数据。
这并不意味着原始数据本身是混乱的。它只是意味着数据以不适合我们想要进行的分析的方式组织。通常,为此目的整理数据涉及重塑其结构,这正是我们将在本视频中介绍的内容。
我们拥有的数据以不适合分析的方式组织。我们希望重新组织数据以继续进行分析。当然,我们将使用 `tidyverse` 中值得信赖的工具来完成这项工作。
我们拥有的数据是关于杂货销售的。每一行代表一个顾客。数据中有两位顾客。每一列是他们购买的商品。顾客1购买了三种商品:面包、牛奶和香蕉。顾客2购买了两件商品:牛奶和厕纸。在这种格式下,回答诸如“有多少顾客购买了牛奶?”或“最受欢迎的商品是什么?”等问题会非常麻烦。
相反,我们希望将数据转换为“每位顾客每件商品一行”的格式。
为此,我们将使用 `tidyverse` 工具包中的一个新工具:`tidyr` 包。这个包的目标是通过“数据透视”在宽数据与长数据之间转换,从而帮助您整理数据。我们稍后将更详细地讨论宽数据和长数据的含义。该包还涉及拆分和合并字符列、嵌套与非嵌套列,以及阐明在进行这些操作时应如何处理 `NA` 值。
让我们谈谈将数据从宽格式透视到长格式,或从长格式透视到宽格式。那么,我所说的“透视”是什么意思呢?不是这个(指舞蹈动作)。看过《老友记》的朋友可能会理解这个梗。而是指重组数据的行和列,使相同的信息被记录在多行或多列中。
在宽格式中,我们拥有的原始数据里,每位顾客购买的商品被组织在多列中。数据框更“宽”,因为它有更多的列。在我们希望看到的格式中,购买的商品被组织在多行中。数据框更“长”,因为它有更多的行。
首先,让我们谈谈如何从宽数据变成长数据。要从宽数据变成长数据,我们将使用一个恰如其名的函数:`pivot_longer`。该函数的第一个参数是数据,这与大多数 `tidyverse` 函数一样。然后,我们指定要透视哪些列。在下一个参数中,我们指定一个列名,用于存放被透视变量的原始列名。让我们暂停一下,这有点拗口。请记住,在原始数据中,我们有像 `item1`、`item2`、`item3` 这样的列名。这个参数指定了如何组织它们,即给它们将要进入的列起什么名字。
请注意,这是一个字符串。它不是我们数据中已经存在的变量,因此我们将要创建的变量名称作为字符串提供。
在第四个也是最后一个参数中,我们命名一个列,用于存放被透视变量中的数据值。同样,由于变量尚未创建,我们以字符串形式提供要创建的变量名称。
综上所述,我们想要透视名为 `item1` 到 `item3` 的列(冒号运算符表示“到”或“通过”,是列出这些变量之间每个变量名的快捷方式)。原始数据中的列名(`item1`、`item2` 和 `item3`)将被组织到一个名为 `item_no` 的新列中。而商品本身,即原始数据单元格中的值,将被组织到一个名为 `item` 的列中。
结果是一个 6 行 3 列的数据框。但是请注意,最后一行有一个 `NA`,因为顾客2只购买了两件商品。当数据是这种格式时,为不存在的 `item3` 保留一行似乎很傻。所以让我们处理那个 `NA`。我们可以在 `pivot_longer` 函数中添加另一个参数:`values_drop_na`,以指定如何处理这一行。这里我们将其设置为 `TRUE`,意味着我们希望删除在 `values_to` 列中只包含 `NA` 的行。
那么结果就是一个 5 行 3 列的数据框:顾客1的三件商品,顾客2的两件商品。
将所有内容整合在一起,并将得到的长数据框命名为 `purchases`,我们得到一个数据框,其中每一行对应一个顾客-商品组合。这个数据框的行数更多(5行对比2行),列数更少(3列对比4列),因此比我们开始的宽数据框更长。
请注意,我们并没有制定关于什么是宽或长数据框的硬性规定。因此,我们说“更宽”或“更长”是相对于我们原始的起点而言的。
那么,为什么要进行数据透视呢?很可能是因为你分析的下一步需要它。例如,假设我们有一个单独的商品价格数据框。我们可以将这两个数据框连接到我们的 `purchases` 数据。我们很快就会讲到连接,但你可以将其理解为将价格信息引入我们的 `purchases` 数据框中。
既然你已经了解了如何透视为长数据,让我们考虑从长数据透视回宽数据的场景。我们将从创建的 `purchases` 数据开始,将其透视回宽格式,即每位顾客一行,商品组织在各列中的原始格式。
为此,我们使用 `pivot_wider` 函数,其中第一个参数照例是数据。第二个参数是 `names_from`,它告诉我们长格式中的哪一列(或哪些列)包含的内容应成为宽格式中的列名。第三个也是最后一个参数是 `values_from`,它告诉我们长格式中的哪一列(或哪些列)包含的内容应成为宽格式中新列的值。
在本视频中,我们介绍了从宽到长和从长到宽的数据透视。我们还定义了 `pivot_longer` 和 `pivot_wider` 函数的基本参数。我们甚至提到了一个可选参数,它告诉我们如何处理 `NA` 值。这两个函数都有大量的参数,其中一些我们会在课程中介绍,另一些在你继续使用这些函数时可能会发现很有用。和往常一样,阅读函数文档是熟悉这些附加参数的最佳方式。


本节课中我们一起学习了数据整理的核心操作——数据透视。我们掌握了如何使用 `pivot_longer` 将宽格式数据转换为更易于分析的长格式,以及如何使用 `pivot_wider` 将长格式数据转换回宽格式。理解并应用这些函数是进行有效数据清洗和准备的关键步骤。
# 020:随时间变化的国家人口 📈


在本节课中,我们将学习如何使用R语言和`ggplot2`包,重现一个展示美国、印度和中国从2000年到2023年人口变化的折线图。我们将从读取世界银行的数据开始,逐步完成数据整理、可视化以及图表美化的全过程。
## 加载必要的包
首先,我们需要加载用于数据处理和可视化的核心包。`tidyverse`包集成了多个数据处理和图形工具,而`scales`包则用于优化图表标签的显示格式。
```r
library(tidyverse)
library(scales)
```
## 读取数据
我们的数据来源于世界银行,包含了2000年至2023年各国的人口估计值。数据以宽格式存储,即每个年份的人口数据是一个单独的列。
```r
population <- read_csv("https://central_repository_url/population_data.csv")
```
查看数据的前几行,我们可以看到数据结构:包含国家名称、国家代码以及从`2000`到`2023`的多个列。


```r
head(population)
```
## 理解目标图表
我们的目标是重现一个折线图。该图表的X轴是年份,Y轴是人口(以百万计),并且用不同的颜色和形状区分美国、印度和中国这三个国家。


这意味着,为了使用`ggplot2`绘图,我们的数据框需要包含三个核心变量:`year`(年份)、`population`(人口)和`country_name`(国家名称)。
## 数据重塑:从宽格式到长格式
目前,我们的数据是宽格式的。为了绘图,我们需要将其转换为长格式,即所有年份的人口数据合并到一个`population`列中,并创建一个`year`列来标识年份。这个操作称为“数据透视”或“变长”。
```r
population_longer <- population %>%
pivot_longer(
cols = `2000`:`2023`, # 指定要转换的列
names_to = "year", # 将列名(年份)存入新列“year”
values_to = "population", # 将列值(人口)存入新列“population”
names_transform = as.numeric # 将“year”列从字符型转换为数值型
)
```
转换后,数据框变得“更长”,每一行代表一个国家在特定年份的人口数据。
## 筛选目标国家
我们只关注美国、印度和中国这三个国家。使用`filter`函数从长格式数据中筛选出这些国家的数据。
```r
countries_of_interest <- c("United States", "India", "China")
population_filtered <- population_longer %>%
filter(country_name %in% countries_of_interest)
```
## 创建基础图表
现在,我们可以开始绘制基础图表。使用`ggplot()`函数,将筛选后的数据映射到X轴(`year`)、Y轴(`population`)和颜色(`country_name`)上,并添加点和线。
```r
population_filtered %>%
ggplot(aes(x = year, y = population, color = country_name)) +
geom_point() +
geom_line()
```
## 优化图表细节
基础图表已经成型,但我们需要进行一系列优化,使其更接近目标图表。以下是需要调整的方面:
* **X轴和Y轴刻度**:调整刻度的范围和标签。
* **形状映射**:除了颜色,也用形状区分国家。
* **颜色设置**:手动指定与国家国旗相关的颜色。
* **标签和标题**:添加清晰的轴标签、标题和说明。
* **主题和布局**:使用简洁的主题,并调整图例位置。
我们将逐步添加这些优化层。
### 1. 添加形状映射并调整X轴
在`aes()`映射中添加`shape = country_name`,并使用`scale_x_continuous`设置X轴刻度从2000年到2024年,每4年一个刻度。
```r
population_filtered %>%
ggplot(aes(x = year, y = population, color = country_name, shape = country_name)) +
geom_point() +
geom_line() +
scale_x_continuous(breaks = seq(2000, 2024, by = 4), limits = c(2000, 2024))
```
### 2. 调整Y轴刻度与标签
使用`scale_y_continuous`设置Y轴刻度,并使用`scales::label_number`函数将人口数值除以一百万,并添加“M”(代表百万)后缀,使标签更易读。
```r
population_filtered %>%
ggplot(aes(x = year, y = population, color = country_name, shape = country_name)) +
geom_point() +
geom_line() +
scale_x_continuous(breaks = seq(2000, 2024, by = 4), limits = c(2000, 2024)) +
scale_y_continuous(
breaks = seq(250000000, 1500000000, by = 250000000),
labels = label_number(scale = 1e-6, suffix = "M")
)
```


### 3. 手动设置颜色
创建一个命名向量,为每个国家指定颜色,并通过`scale_color_manual`应用这些颜色。
```r
country_colors <- c("United States" = "#3C3B6E", "India" = "#FF9933", "China" = "#DE2910")
population_filtered %>%
ggplot(aes(x = year, y = population, color = country_name, shape = country_name)) +
geom_point() +
geom_line() +
scale_x_continuous(breaks = seq(2000, 2024, by = 4), limits = c(2000, 2024)) +
scale_y_continuous(
breaks = seq(250000000, 1500000000, by = 250000000),
labels = label_number(scale = 1e-6, suffix = "M")
) +
scale_color_manual(values = country_colors)
```
### 4. 应用主题并添加标签
使用`theme_minimal()`应用简洁主题,并通过`labs()`函数添加图表标题、副标题、坐标轴标签和数据来源说明。
```r
population_filtered %>%
ggplot(aes(x = year, y = population, color = country_name, shape = country_name)) +
geom_point() +
geom_line() +
scale_x_continuous(breaks = seq(2000, 2024, by = 4), limits = c(2000, 2024)) +
scale_y_continuous(
breaks = seq(250000000, 1500000000, by = 250000000),
labels = label_number(scale = 1e-6, suffix = "M")
) +
scale_color_manual(values = country_colors) +
theme_minimal() +
labs(
title = "Country Populations Over the Years",
subtitle = "2000 to 2023",
x = "Year",
y = "Population (in millions)",
caption = "Data Source: World Bank"
)
```
### 5. 最终调整:图例位置与标题
最后,将图例移动到图表顶部,并通过在`scale_color_manual`和`scale_shape_manual`中设置`name = NULL`来移除图例标题。


```r
final_plot <- population_filtered %>%
ggplot(aes(x = year, y = population, color = country_name, shape = country_name)) +
geom_point() +
geom_line() +
scale_x_continuous(breaks = seq(2000, 2024, by = 4), limits = c(2000, 2024)) +
scale_y_continuous(
breaks = seq(250000000, 1500000000, by = 250000000),
labels = label_number(scale = 1e-6, suffix = "M")
) +
scale_color_manual(values = country_colors, name = NULL) +
scale_shape_manual(values = c(16, 17, 15), name = NULL) + # 16=圆点,17=三角,15=方点
theme_minimal() +
theme(legend.position = "top") +
labs(
title = "Country Populations Over the Years",
subtitle = "2000 to 2023",
x = "Year",
y = "Population (in millions)",
caption = "Data Source: World Bank"
)


print(final_plot)
```
## 总结

本节课中,我们一起学习了如何利用R语言重现一个复杂的数据可视化图表。我们首先使用`pivot_longer`函数将宽格式数据转换为适合`ggplot2`绘图的长格式数据。接着,通过筛选、映射美学属性(颜色、形状)、精细调整坐标轴刻度与标签、手动配置颜色、应用主题以及优化图例布局等一系列步骤,逐步将一个基础图表优化为信息清晰、视觉美观的最终成果。这个过程涵盖了数据科学中从数据整理到可视化呈现的核心工作流。
# 021:数据连接 🧩

在本节课中,我们将学习如何将多个数据框基于共同信息连接在一起。我们将使用 `tidyverse` 包中的函数,特别是那些以 `_join` 结尾的函数,来合并数据。
## 概述
到目前为止,我们主要使用 `dplyr` 和 `tidyr` 包处理单个数据框。本节将介绍如何连接多个数据框,以整合来自不同来源的信息。我们将使用一个关于10位改变世界的女科学家的数据集作为示例。
## 数据背景
我们的数据来自《发现》杂志的一篇文章,包含三位科学家的信息,分布在三个数据框中:
* `professions`:包含10位科学家的姓名和职业。
* `dates`:包含8位科学家的出生和去世年份(部分科学家仍健在,去世年份为 `NA`)。
* `works`:包含9位科学家的姓名及其主要成就。
我们的目标是创建一个包含10行数据(每位科学家一行)的最终数据框,整合姓名、职业、出生年份、去世年份和主要成就。由于信息不完整,最终结果中会出现 `NA` 值。
## 连接函数简介
`tidyverse` 提供了一系列以 `_join` 结尾的函数,用于连接数据框。函数名的第一个词指明了连接的类型。例如:
* `left_join()`:保留左侧数据框的所有行。
* `right_join()`:保留右侧数据框的所有行。
* `full_join()`:保留两个数据框的所有行。
* `inner_join()`:仅保留两个数据框共有的行。
* `semi_join()`:筛选出左侧数据框中与右侧数据框匹配的行,但不引入右侧数据框的列。
* `anti_join()`:筛选出左侧数据框中与右侧数据框**不**匹配的行。
为了更清晰地理解这些函数,我们将使用一个简单的示例数据。
### 示例数据
定义两个小型数据框 `x` 和 `y`:
```r
x <- tibble(id = c(1, 2, 3), value_x = c("x1", "x2", "x3"))
y <- tibble(id = c(1, 2, 4), value_y = c("y1", "y2", "y4"))
```
数据框 `x` 包含 id 1, 2, 3;数据框 `y` 包含 id 1, 2, 4。id 1 和 2 是两个数据框共有的。
## 各类连接操作详解
上一节我们介绍了连接的基本概念,本节我们将通过示例数据具体看看每种连接的操作和结果。
### 左连接 (`left_join`)
左连接保留左侧数据框(`x`)的所有行,并从右侧数据框(`y`)匹配相应的行。对于 `x` 中存在但 `y` 中不存在的行,来自 `y` 的列将用 `NA` 填充。
以下是左连接的代码和结果:
```r
left_join(x, y)
```
结果将包含 id 为 1, 2, 3 的行。id 3 对应的 `value_y` 为 `NA`。
现在,让我们将左连接应用于科学家数据。我们将 `professions` 与 `dates` 进行左连接,这意味着我们希望保留 `professions` 中的所有科学家,并尝试从 `dates` 中添加他们的日期信息。
```r
professions %>%
left_join(dates)
```
执行此操作后,我们将得到一个包含10行的数据框(来自 `professions`),其中8位在 `dates` 中有记录的科学家将获得出生和去世年份,而另外2位科学家(如 Ada Lovelace)的日期列将为 `NA`。
请注意代码中管道运算符 `%>%` 的使用,它使代码更易读。`left_join(professions, dates)` 与 `professions %>% left_join(dates)` 是等价的。
### 右连接 (`right_join`)
右连接与左连接相反,它保留右侧数据框(`y`)的所有行,并从左侧数据框(`x`)匹配相应的行。
以下是右连接的代码和结果:
```r
right_join(x, y)
```
结果将包含 id 为 1, 2, 4 的行。id 4 对应的 `value_x` 为 `NA`。
在科学家数据的例子中,对 `professions` 和 `dates` 进行右连接,将只保留 `dates` 中的8位科学家,并添加他们的职业信息(由于这8位科学家都在 `professions` 中,因此不会有 `NA`)。
### 全连接 (`full_join`)
全连接保留两个数据框中的所有行。如果一个观测只存在于一个数据框中,则来自另一个数据框的列将用 `NA` 填充。
以下是全连接的代码和结果:
```r
full_join(x, y)
```
结果将包含 id 为 1, 2, 3, 4 的所有行。id 3 的 `value_y` 和 id 4 的 `value_x` 为 `NA`。
对 `professions` 和 `dates` 进行全连接,将得到所有10位科学家和8位有日期记录的科学家,总共10行,缺失日期信息的行用 `NA` 表示。
### 内连接 (`inner_join`)
内连接只保留两个数据框中共有的行。
以下是内连接的代码和结果:
```r
inner_join(x, y)
```
结果将只包含 id 为 1 和 2 的行。
在科学家数据中,对 `professions` 和 `dates` 进行内连接,将只得到那8位在两个数据框中都存在的科学家的信息。
### 半连接 (`semi_join`)
半连接根据右侧数据框筛选左侧数据框的行,但**不**将右侧数据框的任何列引入结果中。它只检查匹配关系。
以下是半连接的代码和结果:
```r
semi_join(x, y)
```
结果将只包含 id 为 1 和 2 的行,且只显示 `value_x` 列。
在科学家数据中,`semi_join(professions, dates)` 可用于找出 `professions` 中哪些科学家也出现在 `dates` 中,而不引入具体的日期信息。
### 反连接 (`anti_join`)
反连接是内连接的反面。它返回左侧数据框中那些在右侧数据框中**没有**匹配项的行。
以下是反连接的代码和结果:
```r
anti_join(x, y)
```
结果将只包含 id 为 3 的行。
在科学家数据中,`anti_join(professions, dates)` 会告诉我们哪些科学家在 `professions` 中有记录,但在 `dates` 中没有。这在数据准备阶段非常有用,可以提示我们需要查找并补充哪些科学家的日期信息。
## 连接键与注意事项
在执行连接时,R 会自动查找两个数据框中共有的列名作为连接键(例如 `name` 列)。如果列名不同,需要使用 `by` 参数明确指定,例如 `left_join(x, y, by = c("id_x" = "id_y"))`。
连接键的值必须完全一致(包括大小写和空格),连接才能成功匹配。
## 整合科学家数据
现在,让我们将所有科学家的信息整合到一个数据框中。我们的策略是:首先将 `professions` 与 `dates` 左连接,然后将结果再与 `works` 进行左连接。
```r
complete_scientists <- professions %>%
left_join(dates) %>%
left_join(works)
```
这段代码意味着:我们首先保留 `professions` 中的所有科学家,并加入他们的日期信息;然后,在这个结果的基础上,再次保留所有行,并加入他们的成就信息。最终,我们将得到一个包含10行、5列(姓名、职业、出生年、去世年、成就)的数据框,缺失信息处显示为 `NA`。
## 如何选择连接类型
如果你不确定该使用哪种连接,建议从 `left_join` 开始,然后尝试 `right_join` 和 `full_join`,以观察数据的变化。在数据量较小、易于直观检查时,正确的选择通常比较明显。当数据量变大时,则需要仔细思考选择某种连接类型会导致丢失哪些观测。
## 总结

本节课中,我们一起学习了如何使用 `tidyverse` 中的 `*_join` 系列函数来连接多个数据框。我们详细探讨了左连接、右连接、全连接、内连接、半连接和反连接的区别与应用场景,并成功将分散在三个数据框中的科学家信息整合到了一起。掌握数据连接是进行复杂数据分析的关键步骤。
# 022:大洲人口 🌍📊


在本节课中,我们将继续探索人口数据,并引入关于各国所在大洲的新信息,学习如何进行数据连接。我们的最终目标是创建一个展示各大洲总人口的条形图。我们将使用两个输入数据集:`countries_and_populations` 和 `countries_and_continents`。这两个数据集都包含国家信息,我们将以此为基础进行连接。
我们将使用 `tidyverse` 和 `scales` 这两个R包。`tidyverse` 用于数据处理和绘图,`scales` 包则用于美化图表中的坐标轴标签。
## 数据准备
在分析和可视化数据之前,我们需要进行一些数据准备工作。本次分析聚焦于最新的可用人口数据(2023年),因此我们需要修改人口数据框。
首先,我们只选择2023年的人口数据。由于不同年份的数据分布在不同的列中,我们使用 `select` 语句来筛选列。
以下是需要选择的列:
* 所有标识信息列(从 `series_name` 到 `country_code`)
* 2023年的人口数据列(由于列名是数字,需要用反引号 `` `2023` `` 引用)
```r
population_2023 <- population %>%
select(series_name, series_code, country_name, country_code, `2023`)
```
接下来,为了后续引用方便,我们将 `2023` 列重命名为 `population`。
```r
population_2023 <- population_2023 %>%
rename(population = `2023`)
```
## 数据连接
现在,我们需要将人口数据与大洲数据连接起来。首先,查看两个数据框的列名,以确定用于连接的共同变量。
* `population_2023` 包含:`series_name`, `series_code`, `country_name`, `country_code`, `population`
* `continents` 包含:`entity`, `code`, `year`, `continent`
两个数据框的共同信息可以是国家名称或国家代码。建议尽可能使用代码进行连接,因为代码通常是标准化的,可以避免因国家名称拼写差异或历史变更导致的问题。
因此,我们将使用 `population_2023` 中的 `country_code` 变量和 `continents` 中的 `code` 变量进行连接。
我们希望创建一个新的数据框,保留 `population_2023` 中的所有行和列,并从 `continents` 数据框中引入对应的大洲信息。这需要使用左连接(`left_join`)。
```r
population_continents <- population_2023 %>%
left_join(continents, by = c("country_code" = "code"))
```
连接后,我们可能只关心 `continent` 列。可以在连接时或连接后选择所需的列,以简化数据框。
## 处理缺失值
连接后,检查新创建的 `population_continents` 数据框。有些国家在 `continents` 数据框中没有对应记录,导致其 `continent` 变量为 `NA`。
我们可以通过筛选 `is.na(continent)` 来识别这些国家。
```r
population_continents %>%
filter(is.na(continent))
```
结果显示有两个这样的国家:Kosovo 和 Channel Islands。这可能是因为它们在 `continents` 数据框中的代码与 `population_2023` 中的 `country_code` 不完全匹配。
为了解决这个问题,我们需要在连接前修改 `population_2023` 数据框中的 `country_code`,使其与 `continents` 数据框中的代码匹配。
我们使用 `mutate` 函数和 `case_when` 语句来条件性地修改代码:
* 如果国家名是 “Kosovo”,则使用 `continents` 中对应的代码(例如 “OWID_KOS”)。
* 如果国家名是 “Channel Islands”,则使用 `continents` 中对应的代码(例如 “OWID_CIS”)。
* 对于其他国家,保持原 `country_code` 不变。
```r
population_2023_modified <- population_2023 %>%
mutate(country_code = case_when(
country_name == "Kosovo" ~ "OWID_KOS",
country_name == "Channel Islands" ~ "OWID_CIS",
.default = country_code
))
```
修改后,再次执行左连接,并检查是否还存在 `NA` 值。
```r
population_continents <- population_2023_modified %>%
left_join(continents, by = c("country_code" = "code"))
population_continents %>%
filter(is.na(continent)) # 现在应该返回0行
```
## 数据汇总
我们的目标是创建一个按大洲汇总总人口的数据框。这需要先按 `continent` 分组,然后对 `population` 列求和。
```r
p_summary <- population_continents %>%
group_by(continent) %>%
summarise(total_population = sum(population))
```
`p_summary` 数据框现在包含每个大洲及其总人口。
## 创建条形图
现在,我们可以使用 `p_summary` 数据框创建条形图。在X轴上放置 `continent`,在Y轴上放置 `total_population`。
由于我们已经计算好了每个条形的高度(即总人口),我们使用 `geom_col()` 而不是 `geom_bar()`。
```r
ggplot(p_summary, aes(x = continent, y = total_population)) +
geom_col()
```
这样就得到了一个基本的条形图,展示了各大洲的总人口。
## 创建棒棒糖图
接下来,我们尝试重新创建一个棒棒糖图(lollipop chart)。在这种图表中,数据以点和连接到点的线段形式表示,人口在X轴,大洲在Y轴。
首先,我们翻转坐标轴,将 `continent` 映射到Y轴,`total_population` 映射到X轴。然后,我们添加点图层(`geom_point()`)和线段图层(`geom_segment()`)。
对于 `geom_segment()`,我们需要定义线段的起点和终点:
* 起点:`x = 0`, `y = continent`
* 终点:`x = total_population`, `y = continent`
为了提高代码可读性,我们将美学映射定义在各自的图层中,而不是全局 `aes()` 中。
```r
ggplot(p_summary) +
geom_segment(aes(x = 0, xend = total_population, y = continent, yend = continent)) +
geom_point(aes(x = total_population, y = continent))
```
## 美化图表


最后,我们对图表进行美化,使其更接近目标样式:
1. 使用 `theme_minimal()` 改变主题。
2. 添加适当的标签:X轴(总人口)、Y轴(大洲)、标题(世界人口)、副标题(截至2023年)和说明(数据来源)。
3. 使用 `scales` 包的 `label_number()` 函数格式化X轴标签,将数字以“十亿”为单位显示。
```r
ggplot(p_summary) +
geom_segment(aes(x = 0, xend = total_population, y = continent, yend = continent)) +
geom_point(aes(x = total_population, y = continent)) +
theme_minimal() +
labs(
x = "Total Population",
y = "Continent",
title = "World Population",
subtitle = "as of 2023",
caption = "Data sources: The World Bank and Our World in Data"
) +
scale_x_continuous(labels = label_number(scale = 1e-9, suffix = " B"))
```
渲染整个文档以确保没有错误,并且可视化效果良好。

## 总结



本节课中,我们一起学习了如何连接两个相关的数据集(人口和大洲信息),处理连接时出现的缺失值问题,按类别对数据进行汇总,并创建了两种类型的图表(条形图和棒棒糖图)来可视化各大洲的总人口。我们还学习了如何使用 `scales` 包来格式化坐标轴标签,以及如何通过添加图层和修改主题来美化 `ggplot2` 图形。
# 023:数据类型


## 概述
在本节课中,我们将学习R语言中的数据类型。理解数据类型是有效进行数据科学分析的基础。我们将探讨为什么数据类型很重要,认识R中常见的数据类型,并学习如何处理类型转换和特殊值。
## 为什么需要关心数据类型?
上一节我们介绍了R语言及其在数据科学中的应用。本节中,我们来看看为什么理解数据类型至关重要。我们将通过一个关于“爱猫人士”的假设性调查来说明。
假设一项调查询问了受访者的姓名和拥有的猫的数量,并且要求以数字形式输入猫的数量。我们从CSV文件中读取数据并查看。
以下是数据的前几行:
* John Doe: 2
* Jane Smith: 1
* Bob Johnson: 0
一个合理的问题是:受访者平均拥有多少只猫?我们使用`summarize`函数和`mean`函数来计算平均值。
```r
cat_lovers %>%
summarize(mean_cats = mean(number_of_cats))
```
不幸的是,结果返回了`NA`和一个冗长的警告。代码没有报错,但结果并非预期。警告指出,`mean`函数的参数不是数值型(numeric)或逻辑型(logical)。我们知道“猫的数量”应该是数值变量,问题出在哪里?
## 深入探究数据
既然函数本身没问题,我们就需要仔细检查数据。让我们查看数据框的结构。
```r
glimpse(cat_lovers)
```
在变量名旁边,我们看到括号里有`<chr>`,这代表字符型(character string)。姓名是字符型很合理,但“猫的数量”变量也是字符型就不对了。让我们再看一下数据。
前几个观测值看起来正常,但后续一些受访者没有按照指示填写。例如,Doug Bass填写了“three”而不是数字“3”,而Ginger Clark则填写了“two and a half human”。由于这些字符型回答,整个“猫的数量”列在读取数据时被强制转换成了字符型。这就是我们尝试计算平均值时得到`NA`和警告的原因。
## 数据清洗与类型转换
接下来,我们需要清理Doug和Ginger的回答。我们将Ginger的猫数量设为2,Doug的设为3,并将所有人的回答转换为数值型。
```r
cat_lovers_clean <- cat_lovers %>%
mutate(
number_of_cats = case_when(
name == "Ginger Clark" ~ "2",
name == "Doug Bass" ~ "3",
TRUE ~ number_of_cats
),
number_of_cats = as.numeric(number_of_cats)
)
```
然后我们再次尝试计算平均值。
```r
cat_lovers_clean %>%
summarize(mean_cats = mean(number_of_cats))
```
这次我们得到了一个平均值,但仍有警告。警告提示我们仍有类型转换问题。让我们换一种方式:先将“2”和“3”作为字符型处理,然后一次性将所有值转换为数值型。
```r
cat_lovers_clean <- cat_lovers %>%
mutate(
number_of_cats = case_when(
name == "Ginger Clark" ~ "2",
name == "Doug Bass" ~ "3",
TRUE ~ number_of_cats
)
) %>%
mutate(number_of_cats = as.numeric(number_of_cats))
cat_lovers_clean %>%
summarize(mean_cats = mean(number_of_cats, na.rm = TRUE))
```
成功了!现在我们不仅得到了平均猫数,也解决了警告问题。
这个故事的启示是:如果你的数据表现不符合预期,读取数据时的类型强制转换可能是原因。要解决这个问题,你需要理解数据类型和类型转换。
## R中的常见数据类型
既然我们已经有了学习R数据类型的充分动机,现在就来正式学习它们。在数据科学中,你最常遇到的数据类型包括逻辑型、双精度型、整型和字符型。
以下是这些类型的简要说明:
* **逻辑型**:即布尔值,如`TRUE`和`FALSE`。注意它们是大写的。
* **字符型**:即字符串。它们总是被引号包围。根据tidyverse风格指南的建议,我们通常使用双引号。
* **双精度型**:即浮点数值,是R中默认的数值类型。简单来说,就是可以包含小数的数字。
* **整型**:即整数。它们总是整数。在R中,通过在数字后添加字母`L`来创建整数。例如,`5L`。另外,如果你要求R生成一个数字序列(如`1:3`),它会默认你希望得到整数。
## 向量与类型一致性
我们可以使用`c()`函数即时创建向量。向量中的所有值必须是相同类型,正如我们在爱猫人士例子中看到的。
```r
# 双精度型向量
c(1.5, 2.8, 3.1)
# 字符型向量
c("apple", "banana", "cherry")
```
## 类型转换
正如爱猫人士的例子所示,我们可以在数据类型之间进行转换。
以下是显式类型转换的例子:
```r
# 将整数向量转换为字符型
as.character(c(1L, 2L, 3L))
# 将逻辑型转换为数值型(TRUE为1,FALSE为0)
as.numeric(c(TRUE, FALSE, TRUE))
```
然而,类型转换也可能无意中发生。当你创建一个包含不同类型数据的向量时,R会自动进行强制转换。
以下是隐式类型转换的规则:
* 字符型与任何类型组合:所有元素都转换为字符型。因为数字可以表示为字符串,但字符串不一定能表示为数字。
* 双精度型与整型组合:所有元素都转换为双精度型。因为整数可以表示为双精度数,但反之则会丢失小数部分。
* 逻辑型与整型组合:所有元素都转换为整型(TRUE为1,FALSE为0)。
## 特殊值
就像人类语言有特例一样,编程语言也有。R中的一些特殊值包括:
* `NA`:表示“不可用”(Not Available),即缺失值。
* `NaN`:表示“非数字”(Not a Number),例如`0/0`的结果。
* `Inf` 和 `-Inf`:表示正无穷和负无穷。
我们将重点关注`NA`,因为它在数据处理中最常见。当对包含`NA`的向量应用某些函数时,操作可能会失败。`NA`通常需要特殊处理,例如在应用函数前显式移除它们。
```r
# 使用 na.rm 参数忽略 NA
mean(c(1, 2, NA, 4), na.rm = TRUE)
# summary 函数会显示 NA 的数量
summary(c(1, 2, NA, 4))
```
最后一个重点是:`NA`本身实际上是逻辑型。为什么?因为`NA`代表未知值,但它可以在逻辑运算中发挥作用。考虑以下情况:
```r
TRUE | NA # 返回 TRUE
FALSE | NA # 返回 NA (在R中实际返回NA,但逻辑上应为FALSE?需要澄清)
```
解释:`TRUE | NA` 返回 `TRUE`,因为无论未知的`NA`是`TRUE`还是`FALSE`,结果都是`TRUE`。而`FALSE | NA`的结果取决于`NA`的实际值,由于未知,所以返回`NA`。这在数学运算中可能不太直观,但在处理缺失数据的背景下是有意义的。
## 总结

本节课中,我们一起学习了R语言中的数据类型。我们首先通过一个调查数据的例子理解了为什么数据类型至关重要。然后,我们认识了四种常见的数据类型:逻辑型、字符型、双精度型和整型。我们学习了如何创建向量,并理解了向量中所有元素必须类型一致的原则。接着,我们探讨了显式和隐式的类型转换,以及转换时的优先级规则。最后,我们介绍了`NA`等特殊值及其在处理缺失数据时的行为。掌握这些概念将帮助你更有效地诊断数据问题并进行数据清洗。
# 024:这是我的类型 🧩


在本节课程中,我们将回顾R语言中关于数据类型的知识,并特别聚焦于**隐式强制转换**。我们将通过五个具体的向量示例,来理解当不同类型的数据组合在一起时,R语言会如何处理它们。理解这一点对于数据科学家至关重要,因为它经常在后台自动发生,可能导致意想不到的结果。当你发现数据行为异常时,或许可以思考:“是不是发生了隐式类型转换?”
---
## 概述
我们将逐一分析五个包含不同数据类型的向量。对于每个向量,我们会先尝试猜测其最终的数据类型,然后使用R代码进行验证。核心方法是使用 `typeof()` 函数来检查每个元素以及整个向量的类型。
---
## 示例一:数字与字符的混合
首先,我们来看一个包含数字和字符的向量。
以下是构成该向量的元素:
* `1`:一个双精度浮点数。
* `1L`:一个整数。
* `"c"`:一个字符。
让我们检查每个元素的类型:
```r
typeof(1) # 输出:"double"
typeof(1L) # 输出:"integer"
typeof("c") # 输出:"character"
```
这个向量包含了一个双精度数、一个整数和一个字符。那么,整个向量的类型会是什么呢?
我的猜测是:它会被强制转换为**字符**类型。因为数字可以轻松地加上引号变成字符,但字符无法直接变成数字。
现在,我们来验证一下:
```r
vec1 <- c(1, 1L, "c")
typeof(vec1) # 输出:"character"
```
结果证实了我们的猜测。当字符出现在向量中时,其他所有元素通常都会被强制转换为字符类型。
---

## 示例二:运算结果与字符的混合
上一节我们看到了字符的强大“同化”能力。本节中,我们来看一个更复杂的例子。
这个向量包含:
* `1L / 0`:一个整数除以零的运算结果。
* `"a"`:一个字符。
我们先看看这个向量长什么样,并检查每个部分的类型:
```r
vec2 <- c(1L / 0, "a")
print(vec2) # 输出:[1] "Inf" "a"
typeof(1L / 0) # 输出:"double"
typeof("a") # 输出:"character"
```
`1L / 0` 的结果是无穷大(`Inf`),其类型是双精度浮点数。另一个元素是字符。那么,当双精度数和字符结合时,结果会是什么?
根据之前的经验,字符类型会胜出。让我们验证:
```r
typeof(vec2) # 输出:"character"
```
再次证明,一旦向量中混入字符,整体类型就是字符。
---
## 为什么这很重要?
你可能会问,解决这些“谜题”有什么实际意义?除了有趣和帮助我们理解R语言的特性,更重要的是实际应用。
在数据分析中,我们经常处理数据框。数据框的每一列本质上都是一个向量。如果某一列中混合了不同类型的数据(例如,在调查问卷中,有些人用数字“3”回答,有些人用文字“三”或“3+”回答),那么在读取数据时,R语言会进行隐式强制转换,通常会将整列都转换为字符类型。这可能会影响后续的数值计算和分析。因此,了解类型转换的规则,有助于我们在数据清洗和预处理阶段发现问题。
---
## 示例三:整数与双精度数的混合
现在,我们来看一个不涉及字符的例子。
这个向量包含:
* `1:3`:一个从1到3的整数序列。
* `5`:一个数字。
它们看起来都是整数。我们来检查一下:
```r
typeof(1:3) # 输出:"integer"
typeof(5) # 输出:"double"
```
有趣!`1:3` 生成了一个整数序列,但单独的数字 `5` 在R中默认是双精度浮点数。所以,我们实际上是在混合整数和双精度数。
那么,当整数和双精度数结合时,会发生什么?R语言会将整数“升级”为双精度数,因为所有整数都可以无损地表示为双精度数(例如,1变成1.0),但反过来则不行。因此,整体类型应该是双精度数。
验证一下:
```r
vec3 <- c(1:3, 5)
typeof(vec3) # 输出:"double"
```
我们的直觉是正确的。
---
## 示例四:数字与数字字符的混合
这个例子模拟了真实数据中常见的情况。
向量包含:
* `3`:一个数字。
* `"3+"`:一个看起来像数字的字符。
我们先检查类型:
```r
typeof(3) # 输出:"double"
typeof("3+") # 输出:"character"
```
一个是双精度数,一个是字符。这和我们第一个例子类似。请猜测一下最终类型。
根据规则,字符类型会主导。让我们确认:
```r
vec4 <- c(3, "3+")
typeof(vec4) # 输出:"character"
```
再次印证了之前的规律。
---
## 示例五:逻辑值的组合
最后,我们来看一个简单的例子。
这个向量包含两个逻辑值:
* `NA`:代表缺失值。
* `TRUE`:逻辑真。
你可能知道 `NA` 很特殊,但它也有自己的类型。让我们检查:
```r
typeof(NA) # 输出:"logical"
typeof(TRUE) # 输出:"logical"
```
原来 `NA` 在默认情况下也是逻辑类型。那么,由两个逻辑值组成的向量,其类型应该毫无疑问是逻辑型。
验证一下:
```r
vec5 <- c(NA, TRUE)
typeof(vec5) # 输出:"logical"
```
这里不需要任何强制转换,同类相聚,结果依然是同类。
---
## 总结
本节课中,我们一起通过五个示例深入探讨了R语言中的**隐式强制转换**。我们了解到:
1. **字符类型**具有最高的“强制力”。一旦向量中出现字符,其他所有元素(无论是数字、逻辑值还是其他)通常都会被转换为字符类型。
2. 在**数值类型**内部,当整数和双精度数混合时,整数会被“升级”为双精度数。
3. **同类数据**组合时,不会发生类型转换,例如两个逻辑值组成的向量依然是逻辑型。

理解这些规则有助于你预测R代码的行为,并在数据出现异常时,能够快速想到类型转换可能是问题的根源。记住,在数据分析的开始阶段,仔细检查每一列的数据类型是一个好习惯。
# 025:数据类


在本节课中,我们将要学习R语言中几种重要的数据类。理解这些数据类对于有效地操作和分析数据至关重要。我们将重点介绍因子、日期和数据框这三种核心数据结构。
上一节我们介绍了数据类型,本节中我们来看看数据类。向量就像是乐高积木,我们可以将它们组合在一起,构建更复杂的数据结构。对象的类属性决定了其行为方式,我们将探讨几种在数据处理中常见且重要的类。
## 因子
R语言使用因子来处理分类变量。分类变量具有一组固定且已知的可能取值。
例如,这里有一个表示教育程度的因子变量`x`:
```r
x <- factor(c("BS", "MS", "PhD", "MS"))
```
当我们打印这个向量时,它不仅显示具体的值,还会告诉我们该因子具有的**水平**:BS、MS和PhD。这些水平是该向量中所有可能的唯一取值。
因子的类型是整数,这初看起来可能难以理解。因子的类则是“factor”。一个因子由两部分组成:表示因子水平标签的**字符向量**,以及表示水平编号的**整数向量**。这两部分在内部是“粘合”在一起的。
如果我们查看刚才创建的向量,会发现它是一个具有三个水平(BS、MS、PhD)的因子,其内部值实际上是1、2、3、2。这些整数是R在幕后为每个观测值分配的编号,而标签则是像“BS”、“MS”、“PhD”这样的字符串。R可以方便地在因子和整数之间进行转换。
在处理统计背景下的分类数据时,因子非常重要。我们稍后会给出一个例子。
## 日期
现在让我们看看另一个数据类:日期。这里我有一个看起来像日期的字符串“2025-01-01”,并使用`as.Date`函数将其转换为日期。
```r
y <- as.Date("2025-01-01")
```
如果只打印它,它看起来像一个普通的字符串。但如果询问其类型,会发现它是“double”(双精度浮点数)。为什么日期会是双精度数呢?如果询问其类,则会得到“Date”。我们可以将日期理解为两个部分“粘合”在一起:一个表示自**原点**以来的天数(整数),以及原点日期本身。
例如,查看`y`值(2025年1月1日),将其转换为整数会得到20089。如果将此数字除以365(大约每年的天数),会得到大约55年,这正是1970年到2025年之间的年数。简而言之,在R中构建日期时,可以将这两部分信息(原点1970年1月1日和自原点以来的天数)视为粘合在一起。
## 数据框与列表
另一个我们感兴趣的数据类是数据框。数据框可以看作是**等长向量粘合在一起**形成的。
例如,这里有一个包含两个变量`x`和`y`的数据框,每个变量有两个观测值。如果检查数据框的类型,会发现它是“list”(列表),而其类是“data.frame”。在某种程度上,两个向量粘合在一起就构成了一个数据框。
我们刚刚提到了列表,让我们更仔细地看看它。列表是通用的向量容器,任何类型的向量都可以放入其中。
例如,这里有一个包含三个元素的列表:一个是整数,一个是字符,另一个是逻辑值。
```r
l <- list(1:3, c("a", "b", "c"), c(TRUE, FALSE, TRUE))
```
R允许你将它们全部放入一个名为`l`的对象中,而无需进行任何类型强制转换以使这些向量彼此相似。这就是为什么我们称列表为通用向量容器。如果这些向量长度相等,我们实际上可以将它们组合成一个数据框,而数据框正是我们经常遇到的数据形式。
你可以将数据框视为一种特殊的列表,它包含长度相等的向量。使用`pull`函数,我们可以从数据框中提取向量。虽然我们喜欢将数据保存在数据框中以便在数据管道中处理,但有时将变量(一个向量)从数据框中提取出来也很有用,例如,作为分析的最后一步。
## 因子应用示例
现在我们已经回顾了在R中处理数据时常见的各种数据类,让我们举一个涉及因子操作的例子。
假设我们有一个关于利手习惯的调查数据框。对于每个受访者,我们知道他们的姓名和偏好(是右利手、左利手还是双利手)。这两个变量最初都是字符变量。
我们可以为这些数据制作条形图。在创建条形图时,R在幕后会将偏好向量强制转换为因子,并**按字母顺序**排列该因子的水平。然而,字母顺序很少是最有意义的顺序。
我们可以使用`forcats`包中的`fct_infreq()`函数,按照频率的降序重新排列水平。这个函数会修改偏好变量,并按频率降序重新排序其水平。`forcats`包专门用于处理因子,并提供了许多函数来解决因子的常见问题。

当你拥有真正的分类数据,并且希望覆盖字符向量的默认排序以改善其显示效果时,因子非常有用。否则,所有内容都会按字母顺序排列,这很少是数据可视化或数据摘要所需要的。因子在建模场景中也很有用,我们将在后续的专业课程中涉及这一点。
---

本节课中我们一起学习了R语言中三种核心的数据类:因子、日期和数据框。我们了解了因子的结构和在分类数据中的应用,日期在R内部的表示方式,以及数据框作为等长向量集合的本质。理解这些数据类的特性和行为,是进行有效数据操作和分析的基础。
# 026:减半二氧化碳排放 📊


在本节课中,我们将学习如何使用 `tidyverse` 和 `scales` 包,基于 Gapminder 网站上的一个关于二氧化碳排放的常见误解调查数据,创建一个堆叠条形图。我们将重点练习数据重塑、因子变量排序、颜色自定义以及图形美化。
## 概述
我们将使用来自 Gapminder.org 的数据。该网站提出了一个问题:“如果从今天起将二氧化碳年净排放量减半,全球平均气温会如何变化?”选项包括:下降、保持不变、持续上升。许多人对此存在误解。我们的目标是复现网站上展示各国回答情况的堆叠条形图。

## 数据准备

首先,我们需要加载并查看数据。数据已保存为 CSV 文件,包含八个国家对于“非常错误”、“错误”和“正确”三个选项的回答百分比。
```r
# 加载必要的包
library(tidyverse)
library(scales)
# 读取数据
co2_emissions <- read_csv("path/to/your/co2_emissions_data.csv")
# 注意:数据中不包含“总计”行,只有八个国家的数据。
```
## 数据重塑
原始数据中,每个国家占一行,三个答案类型各占一列。为了绘图,我们需要将其转换为“长”格式,使每一行代表一个国家与一个答案类型的组合。
以下是转换步骤:
```r
co2_emissions_longer <- co2_emissions %>%
pivot_longer(
cols = -country, # 转换除‘country’列之外的所有列
names_to = “answer_type”, # 新列名:存放原列名(即答案类型)
values_to = “percentage” # 新列名:存放原列的值(即百分比)
)
```
运行后,`co2_emissions_longer` 数据框将有 24 行(8个国家 × 3种答案类型),包含 `country`、`answer_type` 和 `percentage` 三列。
## 创建基础堆叠条形图
上一节我们准备好了数据,本节中我们来看看如何创建基础的堆叠条形图。我们将国家放在Y轴,百分比放在X轴,并用颜色填充区分不同的答案类型。
```r
# 创建基础堆叠条形图
ggplot(co2_emissions_longer, aes(x = percentage, y = country, fill = answer_type)) +
geom_col()
```
此时图形已初具雏形,但答案类型和国家的顺序都是默认的字母顺序,与目标图形不符。
## 调整因子顺序
我们的目标图形中,答案类型的顺序是“非常错误”、“错误”、“正确”,而国家的顺序也是特定的。我们需要使用 `forcats` 包的功能来调整因子水平顺序。


以下是调整答案类型顺序的步骤:
```r
co2_emissions_longer <- co2_emissions_longer %>%
mutate(
# 手动指定答案类型的顺序
answer_type = fct_relevel(answer_type, “very wrong”, “wrong”, “correct”),
# 手动指定国家的顺序(顺序来自原始数据框)
country = fct_relevel(country, “Turkey”, “United States”, “Sweden”, “South Africa”, “Pakistan”, “Japan”, “Germany”, “Brazil”)
)
```
调整后,我们发现条形图中各分段的顺序(从下到上)与图例顺序相反。我们可以简单地使用 `fct_rev()` 函数来反转答案类型的顺序,使其在条形图中的堆叠顺序与图例一致。
```r
co2_emissions_longer <- co2_emissions_longer %>%
mutate(
answer_type = fct_rev(answer_type) # 反转因子水平顺序
)
```
## 自定义颜色与图例
现在图形结构正确了,但颜色是默认的。我们需要根据目标图形的配色方案进行自定义。同时,我们需要将图例移动到图形底部。
以下是自定义颜色和调整图例的步骤:
```r
# 定义颜色(使用十六进制码)
custom_colors <- c(
“very wrong” = “#E7553C”,
“wrong” = “#35B0AB”,
“correct” = “#FDE725”
)
# 创建图形,应用自定义颜色并调整图例
ggplot(co2_emissions_longer, aes(x = percentage, y = country, fill = answer_type)) +
geom_col() +
scale_fill_manual(values = custom_colors) + # 应用自定义填充色
theme(legend.position = “bottom”) + # 将图例移动到图形底部
guides(fill = guide_legend(reverse = TRUE)) # 反转图例项顺序,使其与条形图分段顺序匹配
```
## 完善与美化图形
我们的图形已经非常接近目标了。最后一步是进行一些美化工作,例如添加标题、修改主题、调整坐标轴标签格式等。
以下是最终的美化步骤:
```r
final_plot <- ggplot(co2_emissions_longer, aes(x = percentage, y = country, fill = answer_type)) +
geom_col() +
scale_fill_manual(values = custom_colors) +
scale_x_continuous(labels = label_percent(scale = 1)) + # 将X轴标签格式化为百分比(0-100%)
labs(
title = “Survey Results”,
subtitle = “Of the people we have tested, 67% got this question wrong.”,
x = NULL, # 移除X轴标签,因其含义已很明确
y = NULL, # 移除Y轴标签
fill = NULL # 移除图例标题
) +
theme_minimal() + # 使用简洁的主题
theme(legend.position = “bottom”) +
guides(fill = guide_legend(reverse = TRUE))
# 显示图形
print(final_plot)
```
## 总结


本节课中我们一起学习了如何复现一个复杂的堆叠条形图。我们从数据重塑开始,使用 `pivot_longer` 将数据转换为适合绘图的长格式。接着,我们利用 `forcats` 包中的函数调整了因子变量(答案类型和国家)的顺序。然后,我们通过 `scale_fill_manual` 自定义了颜色,并使用 `theme` 和 `guides` 函数将图例移动到底部并调整了其顺序。最后,我们通过添加标签、修改主题和格式化坐标轴,使图形更加清晰美观。这个过程展示了数据准备、图形构建和细节调整在数据可视化中的完整工作流。
# 027:数据导入

## 概述
在本节课中,我们将深入学习如何将数据导入R。我们将探讨可以导入的数据类型,以及在导入数据时需要注意的关键事项,包括文件格式、变量名称和变量类型。
## 矩形数据与CSV文件
矩形数据是最常见的数据存储形式之一。CSV文件是存储矩形数据的常用方式,其中CSV代表逗号分隔值。在这种情况下,分隔符是逗号。其他常见的分隔符包括制表符或分号。
`readr`包是`tidyverse`的一部分,它提供了一系列用于读取分隔表格数据的函数。
以下是`readr`包中的一些核心函数:
* `read_csv()`:用于读取CSV文件。
* `read_csv2()`:用于读取分号分隔的文件。
* `read_tsv()`:用于读取制表符分隔的文件。
* `read_delim()`:允许在函数调用中自定义分隔符。
* `read_fwf()`:用于读取固定宽度文件,需要提供每列字符数的模式。
## 读取与写入CSV文件
上一节我们介绍了`readr`包,本节中我们来看看如何使用它读取和写入CSV文件。
假设我们有一个名为`Nobel`的CSV文件,存储在名为`data`的文件夹中。我们可以使用`read_csv()`函数读取它。
```r
nobel <- read_csv("data/nobel.csv")
```
使用`read_csv()`读取数据的结果是一个`tibble`,这是`tidyverse`中数据框的实现。它会清晰地打印出行数和列数,并智能地显示适合屏幕的内容。
我们也可以将数据框写入CSV文件。首先创建一个小的示例数据框。
```r
df <- tibble(
x = 1:3,
y = c("a", "b", "c")
)
```
然后使用`write_csv()`函数将其写入文件。
```r
write_csv(df, "data/df.csv")
```
之后,我们可以从同一位置读回该文件以进行验证。
```r
read_csv("data/df.csv")
```
## 处理变量名称
导入数据时,变量名称可能不友好。例如,一个数据集中的列名可能包含大写字母、空格或不同的命名约定。
虽然R可以处理这样的变量名,但在使用它们时可能会遇到问题。例如,如果变量名包含空格,在代码中直接使用会导致错误。
一个解决方案是在读取数据时定义列名,使用`col_names`参数。
```r
new_names <- c("id", "name", "bathrooms", "price")
data <- read_csv("data/bad_names.csv", col_names = new_names)
```
另一个便捷的选项是使用`janitor`包中的`clean_names()`函数。该函数可以将列名转换为蛇形命名法(全部小写,空格替换为下划线)。
```r
library(janitor)
data_clean <- data %>% clean_names()
```
## 处理变量类型
R在读取数据时会尝试猜测变量类型,有时需要进行类型转换。例如,一个本应是数值型的列,如果其中包含非数字字符(如“N/A”或“.”),R可能会将其读取为字符型。
处理缺失值表示的一种方法是指定`na`参数。默认情况下,`read_csv()`将空单元格和字符串“NA”视为缺失值。我们可以指定其他表示缺失值的字符串。
```r
data <- read_csv("data/file.csv", na = c("", "NA", ".", "N/A", "9999"))
```
另一种方法是使用`col_types`参数直接指定列的类型。
```r
data <- read_csv("data/file.csv", col_types = cols(
x = col_double(),
y = col_character(),
z = col_logical()
))
```
选择哪种方法取决于你的分析背景和目标,但两者都是工具箱中有用的工具。
`read_csv()`函数支持多种列类型指定方式,详细信息可以在函数文档中找到。
## 从Excel导入数据
除了CSV文件,电子表格(如Microsoft Excel文件)也是常见的表格数据存储方式。`readxl`包提供了`read_excel()`函数来读取Excel文件。
```r
library(readxl)
nobel_excel <- read_excel("data/nobel.xlsx")
```
读取Excel文件的结果同样是一个`tibble`。对于具有不良名称或多种缺失值表示方式的Excel文件,我们可以应用与处理CSV文件相同的修复方法。
* 使用`col_names`参数指定列名。
* 使用`clean_names()`函数清理列名。
* 使用`na`参数指定缺失值字符串。
* 使用`col_types`参数指定列类型(注意,在`read_excel`中,类型是作为字符串指定的,例如“numeric”、“text”、“date”)。
与`read_csv`一样,`read_excel`函数的具体选项可以在其文档中找到。
## 其他数据源
我们深入讨论了CSV文件和Excel电子表格,它们可能是你最常遇到的数据类型。当然,还有许多其他类型的数据可以读入R。
对于几乎任何类型的数据,都有一个R包可以帮助你读取它。例如:
* `googlesheets4`包允许你从Google Sheets读取数据。
* `arrow`包允许你读取Parquet文件(一种常见的大数据存储格式)。
* `DBI`包用于数据库连接。
* `rvest`包允许你从网页抓取非结构化数据。
这些包虽然不都在核心`tidyverse`中,但都设计为能与`tidyverse`良好协作。因此,从本节课详细介绍的数据导入包过渡到这些包并不困难。

## 总结
本节课中,我们一起学习了如何将数据导入R。重要的是要注意文件格式、变量名称和变量类型。我们可以使用`readr`和`readxl`包导入矩形数据,使用`col_names`和`col_types`参数指定列名和类型,并利用`janitor`包中的`clean_names()`等函数来清理变量名。通过理解这些概念,我们可以有效地在R中处理数据,使数据分析过程更加顺畅和高效。
# 028:导入与重新编码 📊


在本节课中,我们将学习如何导入一个略显混乱的调查数据集,并进行数据清洗和重新编码。我们将使用 `tidyverse` 套件,并重点关注变量名的标准化、缺失值处理、变量类型转换以及分类变量的因子化。
## 概述
我们将处理一个关于学龄儿童最喜爱食物及其他信息的小型调查数据集。此数据集虽小,但包含了多种数据格式问题,非常适合演示数据导入和初步清洗的完整流程。
## 数据导入与初步查看
首先,我们需要安装并加载 `tidyverse` 套件。我们将从一个GitHub仓库导入名为 `students-raw.csv` 的原始数据。
```r
# 安装并加载 tidyverse
# install.packages("tidyverse")
library(tidyverse)
# 从指定URL读取原始数据
students_raw <- read_csv("https://raw.githubusercontent.com/your_repo/students-raw.csv")
```
读取数据后,我们查看其结构。数据集包含6名学生,变量包括学生ID、全名、最喜爱的食物、用餐计划和年龄。每个变量的命名规则都不一致,这是我们首先要解决的问题。
## 清洗变量名
变量名混乱不利于后续分析。我们将使用 `janitor` 包中的 `clean_names()` 函数将所有变量名转换为统一的小写蛇形命名法。
```r
# 加载 janitor 包
library(janitor)
# 清洗变量名
students_cleaned <- students_raw %>%
clean_names()
```
`clean_names()` 函数会自动将变量名转为小写,并用下划线替换空格和句点。虽然这简化了命名,但若需要更具描述性的名称,仍需手动修改。
## 处理缺失值
检查数据发现,`favorite_food` 列中存在字符型缺失值 `"N/A"`,而 `age` 列中有一个真正的 `NA`。我们需要统一将它们识别为R中的标准缺失值 `NA`。
我们可以在读取数据时通过 `read_csv()` 函数的 `na` 参数来指定哪些字符串应被视为缺失值。
```r
# 在导入时指定缺失值字符串
students_raw <- read_csv("https://raw.githubusercontent.com/your_repo/students-raw.csv",
na = c("", "NA", "N/A"))
# 随后清洗变量名
students_cleaned <- students_raw %>%
clean_names()
```
现在,两种形式的缺失值都被正确识别为 `NA`。
## 修正变量类型
接下来,我们检查各变量的类型。`age` 列本应是数值型,但由于其中一条记录被填写为字符 `"five"`,导致整列被识别为字符型。我们需要将其转换为数值型。
```r
# 修正年龄变量:将字符“five”转换为数字5,然后转换为数值型
students_cleaned <- students_cleaned %>%
mutate(
age = ifelse(age == "five", 5, age), # 替换特定字符串
age = as.numeric(age) # 转换为数值型
)
```
现在 `age` 列已变为 `double`(双精度浮点数)类型。虽然年龄通常是整数,但保留为 `double` 类型并无大碍。
## 处理分类变量
`meal_plan`(用餐计划)是一个分类变量。为了便于进行分组统计,我们将其转换为因子(factor)类型。
```r
# 将用餐计划转换为因子
students_cleaned <- students_cleaned %>%
mutate(meal_plan = as.factor(meal_plan))
```
转换为因子后,我们可以进行分组分析。例如,计算不同用餐计划学生的平均年龄:
```r
# 按用餐计划分组并计算平均年龄(忽略NA)
students_cleaned %>%
group_by(meal_plan) %>%
summarise(mean_age = mean(age, na.rm = TRUE))
```
默认情况下,因子水平按字母顺序排列。如果我们希望改变顺序(例如将“lunch only”设为第一水平),可以使用 `fct_relevel()` 函数。
```r
# 重新设定因子水平的顺序
students_cleaned <- students_cleaned %>%
mutate(meal_plan = fct_relevel(meal_plan, "lunch only", "breakfast and lunch"))
```
## 保存清洗后的数据
数据清洗完成后,最好将干净的数据集保存下来,以供后续分析使用。通常我们会保存为CSV文件。
```r
# 确保工作目录下存在‘data’文件夹
if (!dir.exists("data")) {
dir.create("data")
}
# 将清洗后的数据保存为CSV文件
write_csv(students_cleaned, "data/students.csv")
```
## 读取保存的数据与数据类型的丢失
现在,我们读取刚保存的CSV文件,并检查变量类型。
```r
# 读取保存的CSV文件
students_reloaded <- read_csv("data/students.csv")
```
我们会发现,之前辛苦转换的 `meal_plan` 因子类型丢失了,它又变回了字符型。这是因为CSV是纯文本格式,无法保存R特有的数据类型信息。
## 使用RDS格式保存数据类型
为了在保存和重新加载时保留所有变量类型(如因子),我们可以使用R的专用数据存储格式 `.rds`。
```r
# 将数据保存为RDS文件
saveRDS(students_cleaned, "data/students.rds")
# 重新读取RDS文件
students_from_rds <- readRDS("data/students.rds")
```
读取 `.rds` 文件后,`meal_plan` 变量仍然保持为因子类型。因此,在需要保留复杂数据结构的清洗步骤与分析步骤之间,使用RDS格式是更佳选择。
## 总结
本节课中,我们一起完成了一个完整的数据导入与清洗流程:
1. **导入数据**:使用 `read_csv()` 并指定缺失值。
2. **标准化命名**:使用 `janitor::clean_names()` 函数。
3. **处理缺失值与类型**:修正了错误的字符型数据并将其转换为正确的数值型。
4. **因子化分类变量**:使用 `as.factor()` 和 `fct_relevel()` 处理分类数据。
5. **保存数据**:学习了CSV格式会丢失数据类型,而RDS格式可以完整保留R对象的所有属性。


掌握这些基本的数据准备技能,是进行任何深入数据分析的坚实基础。
# 029:网络数据爬取

## 概述
在本节课中,我们将学习如何从网页上自动提取数据,即网络爬取。我们将了解网络爬取的基本概念、两种主要场景,并重点介绍如何使用R语言的`rvest`包和Selector Gadget工具来从静态网页中提取数据。
## 什么是网络爬取?
到目前为止,我们导入的都是已经整理成行列形式的规整矩形数据。然而,有时数据以非结构化格式存在,例如在网页上。网络爬取就是从网络上自动提取信息并将其转换为结构化数据集的过程。
网络上可用的数据量日益增长,但这些数据通常以非结构化格式提供。虽然可以手动复制粘贴,但这非常耗时,尤其是在需要处理多个页面时,并且容易出错。
## 网络爬取的两种场景
网络爬取主要有两种场景。我们将重点介绍第一种,但也会简要提及第二种。
第一种场景是**屏幕抓取**。我们使用HTML解析器从网站的源代码中提取数据,这也是我们将要使用的方法。另一种选择是使用正则表达式,手动从包含所需数据的网站源代码片段中提取。虽然这听起来繁琐,但在处理任何文本数据,特别是网站源代码时,繁琐的文本解析是不可避免的。
第二种场景是**从Web API获取数据**。API代表应用程序编程接口。当网站提供一组结构化的HTTP请求,并通常以JSON或XML格式返回文件时,就属于这种情况。在这种场景下,你无需直接处理网站的源代码,而是向网站发出请求并获得实时反馈。
## 举例说明
假设我们想从一个静态网页(如维基百科页面)上抓取一些数据。它的内容会偶尔变化,但大部分文本是静态的。你可以借助HTML解析器,使用一个R包来完成此操作,从中获取数据。
相比之下,考虑从像TikTok或Bluesky这样的社交媒体平台获取数据。这些平台上的时间线在不断变化。因此,如果你想获取特定人物在特定时间的时间线数据,与其尝试抓取你人眼所见的屏幕内容,不如通过其API直接向其数据库发出请求,说明你想要特定人物在特定时间的帖子。
许多网站提供API服务来提供此类数据,有些免费,有些则收费。你可以想象,你手机上使用的许多应用程序,例如天气应用,就使用了这些Web API。出于本课程的目的,我们将专注于屏幕抓取。
## 网页的结构:HTML
在深入了解`rvest`包之前,我们需要先了解一下网页的构成。
如果你曾经制作过网页,哪怕是一个非常简单的网页,你可能对HTML(超文本标记语言的缩写)这几个字母很熟悉。网络上的大部分数据主要以HTML格式提供。HTML是结构化的,或者说它是分层的、基于树的,但它通常不是以适合分析的整洁格式呈现。
一个HTML格式的网页通常如下所示。它以`<html>`标签开始和结束。注意,结束的`</html>`标签用一个斜杠表示。在这些标签内,有一个称为`<head>`的元素,通常是网页的标题;还有一个称为`<body>`的元素,包含页面的其余内容。在`<body>`中,我们有一个由`<p>`标签标识的段落,其中文本居中对齐,内容是“Hello world!”。
如果我们将此页面上的信息作为整洁的数据框导入R,我们可以将`<head>`的内容放入标题行,将`<body>`的内容放入一个单元格。因此,我们需要一些工具来为我们进行这种转换。
## 介绍 `rvest` 包
`rvest`是一个R包,它使HTML数据的基本处理和操作变得相当简单。因此,你本人不需要非常了解HTML的工作原理,只需要有一个大致的概念,足以从中提取内容即可。
`rvest`函数会读取页面并为你提取其中的片段。它们的设计旨在与使用管道运算符构建的管道配合使用,因此非常“整洁”且用户友好。
## 核心 `rvest` 函数
一旦你开始使用`rvest`进行网络爬取,有几个核心函数会经常使用。本幻灯片的目的不是让你死记硬背一个列表,而是让你对这个包的功能有一个大致的了解。
* **`read_html()`**:这通常是我们开始的第一步,它看起来就像我们见过的`read_csv()`或`read_excel()`。它是从URL读取网站源代码的第一步。
* **`html_element()`**:从HTML文档中选择一个指定的元素。
* **`html_elements()`**:一次选择多个指定的元素。
* **`html_table()`**:用于抓取整个表格。
* **`html_text()`**:用于从你抓取的任何节点中提取文本。
* **`html_name()`**:提取标签名称。
* **`html_attr()` 和 `html_attrs()`**:按名称提取每个标签的属性或其值。
现在,我已经向你介绍了这些函数名,很可能其中许多对你来说还不太明白。但我想让你记住的是,除了第一个`read_html()`,所有这些函数都以`html_`开头,并且其中一些函数只是另一个函数的复数版本,例如`element`与`elements`。因此,如果你想提取一个东西,使用`element`;如果你想提取多个东西,使用`elements`。
## 使用 Selector Gadget 工具
我们将使用的另一个促进网络数据抓取的工具是Selector Gadget。虽然网络爬取不需要了解HTML,但你需要能够通过HTML编写的网站源代码中的标签来识别你感兴趣的信息片段。
Selector Gadget是一个交互式工具,有助于发现和选择页面上元素的标签。它是一个开源工具,可以免费使用。你可以将其作为扩展程序添加到浏览器中。`rvest`包中关于使用Selector Gadget的指南会引导你了解成功使用它所需的一切知识。
## Selector Gadget 工作流程
以下是它的工作原理。假设你想从Hacker News获取数据。Hacker News实际上有一个API,因此你可能更愿意使用它,但就目前而言,我们将以今天的Hacker News首页为例,演示如何使用Selector Gadget。
请注意,如果你在观看视频时尝试此操作,你遇到的帖子将会不同,因为这些帖子会随时间变化。
1. 在Chrome上安装Selector Gadget扩展程序后,你应该能在搜索栏旁边看到一个应用图标,看起来有点像插头。点击它时,底部会显示“未找到有效路径”,因为你还没有选择任何内容。
2. 然后,当你点击页面上的一个元素(例如帖子标题)时,你点击的内容会变成绿色,而具有相同标签的其他元素会变成黄色。Selector Gadget还会为该元素生成一个最小的CSS选择器。CSS代表层叠样式表,是一种用于指定网页样式的样式表语言,例如规定具有给定标签的每个元素都将加粗或左对齐等。
3. 在屏幕底部显示“a”的栏中,括号内会显示数字224,这意味着选择了224个具有相同标签的元素。这包括所有黄色的元素加上你实际点击的绿色元素。
4. 如果你随后点击一个高亮显示的元素以将其从选择器中移除,该选择将变为红色。然后,你通过点击和取消点击,直到找到适合你所需项目的正确选择器。
5. 如果无法为恰好是你想要的东西找到一个单一的选择器,你可以提取比你需要的更多的内容,然后使用我们迄今为止学过的其他数据清理工具以及我们将介绍的用于处理文本数据的新工具来清理数据。
这就是使用Selector Gadget的一般过程,它帮助你在无需深入研究网站源代码的情况下,为你感兴趣的元素找到合适的标签。

## 总结
在本节课中,我们一起学习了网络爬取的基础知识。我们了解了从网页提取数据的两种主要方式:屏幕抓取和使用Web API。我们重点介绍了如何使用R语言的`rvest`包来读取HTML页面并提取特定元素。此外,我们还探讨了Selector Gadget这个强大工具,它可以帮助我们直观地识别和选择网页上的元素标签,而无需手动解析复杂的HTML源代码。记住,使用Selector Gadget时,不要期望第一次尝试就能成功,通常需要多次点击尝试才能找到能准确抓取页面所有所需元素的选择器。
# 030:网络爬取注意事项 🕸️

在本节中,我们将探讨进行网络爬取时需要考虑的几个关键方面。这包括伦理考量和技术挑战,以及一个推荐的工作流程。
## 伦理考量 ⚖️
上一节我们介绍了网络爬取的基本工具,本节中我们来看看首先需要考虑的伦理问题。你能爬取数据与你是否应该爬取数据,这是两个不同的问题。对于某些数据源,答案可能一致,但对于其他数据源,尤其是涉及私人个人数据时,答案则截然不同。
这里引用一篇来自Fox.com的文章,标题为《研究人员未经许可发布了7万名OK Cupid用户的个人资料数据》。这个2016年的案例描述了一组研究人员如何从一个当时非常流行的在线约会网站OK Cupid爬取数据。文章指出,这次数据泄露违反了社会科学研究伦理的基本规则,它在未经许可的情况下获取了可识别的个人数据。
研究人员辩称数据已经是公开的,他们并未做错任何事。但细读文章会发现,虽然他们发布的数据集中没有用户的真实姓名,但包含了用户名。有些人使用真实姓名作为用户名,有些人也可能在其他关联其真实姓名的场合使用相同的用户名。诚然,这些用户选择向OK Cupid提供其个人身份信息,但目的是为了参与交友,而非为了让自己的信息以更易搜索的方式作为数据集向公众发布。
当时,这些研究人员的所作所为并不违法,但从伦理上讲,这是错误的。类似案例比比皆是,这促使我们思考数据伦理与行为合法性之间的关系。鉴于法律往往滞后于技术发展——正如我们在当前人工智能发展浪潮中所经历的——对于数据科学家而言,秉持良好的伦理准则行事,而不仅仅依据是否合法,显得尤为重要。数据科学、机器学习和人工智能的伦理是正在发展的研究和政策领域,我们将在后续课程中进一步讨论。但我们可以在教授网络爬取工具的同时,强调这些重要的伦理考量。
## 技术挑战 💻
接下来,我们将讨论网络爬取可能遇到的技术层面的挑战。
### 数据源格式不一致
首先,我们可能面临数据源格式不可靠的问题。以下是杜克大学知名校友页面的截图。可以看到,有些校友有照片,有些则没有。因此,使用相同的选择器,我们获取到的每条条目的信息可能不同。我们或许可以围绕这个问题编写一些额外的逻辑代码,但当数据源格式不可靠时,这类问题会使网络爬取变得具有挑战性。
### 数据分页
另一个潜在问题是数据被分割到多个页面。例如,我访问Yelp并搜索北卡罗来纳州达勒姆的素食餐厅,Yelp的搜索结果每次只显示10条列表,结果分布在许多页面上。为了从这些页面爬取数据,你需要学习编写函数并在多个页面间进行迭代。这是一个可以解决的问题,但无疑增加了另一层挑战。
### 动态内容
此外,这类网站通常包含动态内容,这带来了其特有的挑战,需要学习一套新的工具与之交互。
## 推荐工作流程 📁
在结束之前,让我们对工作流程做最后一点说明。到目前为止,我们一直在Quarto文档中工作。当你在Quarto文档中工作时,每次渲染文档都会重新运行分析。如果在Quarto文档中进行网络爬取,每次渲染时都会重新爬取数据,这既不可靠也不友好。你会不断请求网站提供商的服务器,更重要的是,数据可能会在你操作过程中发生变化,而这可能并非你所愿。
因此,我们将介绍一种替代工作流程:使用R脚本从网络获取数据,然后将其保存为CSV文件或RDS(R数据存储)文件。之后,你可以在Quarto文档的分析中使用这些已保存的数据。在本课程中,到目前为止,你使用的是提供的数据文件,并学习了分析这些数据的工具和技术。通过网络爬取,你将有机会成为收集数据并将其组织成表格格式的人,之后的工作流程与之前相同:在Quarto文档开头读入数据,然后进行分析。我们不希望在每次渲染时重复爬取部分,但我们确实希望Quarto文档中的其余分析是可复现的。
## 总结

本节课中,我们一起学习了网络爬取的重要注意事项。我们首先探讨了爬取数据时的核心伦理问题,强调了“能够”与“应该”之间的区别。接着,我们分析了可能遇到的技术挑战,包括数据源格式不一致、数据分页和动态内容。最后,我们介绍了一个推荐的工作流程,即使用R脚本独立完成数据爬取和保存,再在Quarto文档中进行可复现的分析,以避免每次渲染时重复请求网站并确保数据稳定性。理解这些考量是成为一名负责任且高效的数据科学家的关键一步。
# 031:爬取电商页面 🛒


在本节课中,我们将学习如何从网页上抓取数据。我们将编写一个R脚本,从一个电商示例页面中提取商品信息,并将抓取到的数据保存到本地文件中。

## 概述

我们将通过一个完整的流程来学习网页抓取。首先,我们会创建一个RStudio项目来组织代码。然后,我们将加载必要的R包,检查目标网站是否允许抓取,并使用`rvest`包从网页中提取商品标题、价格和链接。最后,我们会将数据整理成一个整洁的数据框,并保存为CSV文件。
## 创建RStudio项目
首先,我们需要创建一个新的RStudio项目来封装我们的工作。这能确保我们的工作目录始终指向项目文件夹,便于管理代码和数据文件。
在RStudio中,点击“File” -> “New Project”。选择“New Directory”,然后选择“Empty Project”。将项目命名为“web_scraping”并保存到桌面。
创建项目后,当前工作目录会自动设置为该项目文件夹。我们可以通过运行`getwd()`来确认这一点。
接下来,在项目中创建一个新的R脚本文件。点击“File” -> “New File” -> “R Script”,或者使用工具栏上的绿色加号按钮。我们将这个脚本命名为`web_scrape_single_page.R`。


## 加载必要的R包
在编写代码之前,我们需要加载几个关键的R包。我们将使用`tidyverse`进行数据处理,`rvest`进行网页抓取,以及`polite`包来礼貌地检查网站是否允许抓取。
在R脚本中,我们可以使用注释(以`#`开头)来组织代码。在注释后添加四个破折号(`----`)可以创建代码节,方便在脚本中导航。
以下是加载包的代码:
```r
# 加载包 ----
library(tidyverse)
library(rvest)
library(polite)
```
## 检查网站抓取许可
在开始抓取数据之前,我们应该检查目标网站是否允许我们进行抓取。这可以通过网站的`robots.txt`文件来确认,该文件规定了网络爬虫的访问规则。
我们将使用`polite`包中的`bow()`函数来检查一个专门用于学习网页抓取的示例网站:`scrapingcourse.com`。
```r
# 检查抓取许可 ----
bow("https://scrapingcourse.com/")
```
如果函数返回允许抓取的信息,我们就可以继续。作为对比,我们可以尝试检查`facebook.com`,它通常会禁止抓取。
```r
# 对比:一个不允许抓取的网站
bow("https://www.facebook.com/")
```
确认示例网站允许抓取后,我们就可以开始读取网页内容了。
## 读取网页内容


我们将目标网页的URL保存为一个变量,然后使用`rvest`包的`read_html()`函数来获取网页的HTML源代码。
```r
# 读取网页 ----
url <- "https://scrapingcourse.com/"
page <- read_html(url)
```
`page`对象是一个包含网页完整HTML源代码的列表。它是一个`xml_document`类型的对象。我们不需要查看全部代码,只需要使用选择器工具定位我们感兴趣的部分。
## 提取商品标题
我们的目标是提取页面上每个商品的标题、价格和商品详情页的链接。
首先,我们使用浏览器扩展“SelectorGadget”来定位标题对应的HTML标签。点击一个商品标题,工具会显示相关的CSS选择器。我们发现标题的标签是`.title`。
在R中,我们使用`html_elements()`函数配合选择器来提取所有匹配的元素,然后使用`html_text()`函数来获取纯文本内容。
```r
# 提取标题 ----
titles <- page %>%
html_elements(".title") %>%
html_text()
```
现在,`titles`变量是一个包含所有商品标题的字符向量。
## 提取商品链接
接下来,我们需要提取每个商品对应的详情页链接。再次使用SelectorGadget,我们点击商品区域,发现链接信息存储在`a`标签的`href`属性中,选择器可能是`.product-link`。
要提取属性而不是文本,我们使用`html_attr()`函数。
```r
# 提取链接 ----
urls <- page %>%
html_elements(".product-link") %>%
html_attr("href")
```
现在,`urls`变量是一个包含所有商品链接的字符向量。
## 提取商品价格
最后,我们来提取商品价格。使用SelectorGadget点击价格,发现其选择器是`.price`。
我们同样先提取元素,然后获取文本。但价格文本包含美元符号`$`,我们需要将其移除并将其转换为数值型数据。
```r
# 提取并清理价格 ----
prices_raw <- page %>%
html_elements(".price") %>%
html_text()
# 移除美元符号并转换为数值
prices <- prices_raw %>%
str_remove_all("\\$") %>% # 使用转义字符\\$来匹配美元符号
as.numeric()
```
我们使用`stringr`包(`tidyverse`的一部分)的`str_remove_all()`函数来移除所有美元符号。注意,因为`$`在正则表达式中是特殊字符,所以需要用两个反斜杠`\\`进行转义。最后,使用`as.numeric()`将字符转换为数值。
现在,`prices`变量是一个数值向量。
## 整合数据并保存
我们已经成功提取了三个向量:`titles`、`urls`和`prices`。接下来,我们将它们组合成一个整洁的数据框。
```r
# 创建数据框 ----
items <- tibble(
title = titles,
url = urls,
price = prices
)
```
`items`数据框包含了我们从网页上抓取的所有信息。作为最后一步,我们应该再次运行整个脚本,确保没有错误,并且结果符合预期。
为了后续分析,并且避免每次分析都重新访问网站,我们将数据保存到本地。
首先,在项目文件夹中创建一个名为`data`的子文件夹(可以在RStudio的“Files”面板中操作)。然后,使用`write_csv()`函数将数据框保存为CSV文件。

```r
# 保存数据 ----
write_csv(items, "data/items.csv")
```
现在,我们可以在任何R会话中通过`read_csv(“data/items.csv”)`来读取这个数据集,并应用我们学过的所有数据分析工具。
## 总结
在本节课中,我们一起学习了网页抓取的基本工作流程。我们创建了一个RStudio项目来组织工作,使用`polite`包检查了网站的抓取政策,并利用`rvest`包从HTML页面中提取了结构化数据。我们使用了SelectorGadget工具来定位目标元素,并处理了文本数据(如移除货币符号)。最后,我们将提取的数据整合成数据框并保存到本地,为后续的数据分析做好了准备。这个流程是进行网页抓取项目的坚实基础。
# 032:爬取多个电商页面


在本节课中,我们将学习如何从多个网页中爬取数据。我们将基于之前从单个页面爬取数据的知识,扩展到从一个电商网站的多页产品列表中提取信息。通过本教程,你将学会如何构建URL列表、编写可复用的爬取函数,并使用迭代方法高效地收集数据。
---

## 加载必要的R包
首先,我们需要加载完成此任务所需的R包。我们将使用 `tidyverse` 进行数据处理,`rvest` 进行网页抓取,以及 `polite` 包来礼貌地检查网站的可爬取性。在编写代码的过程中,我们意识到还需要 `glue` 包来帮助构建动态URL,因此也将其加入。
```r
# 加载所需包
library(tidyverse)
library(rvest)
library(polite)
library(glue)
```
---

## 构建多页URL列表
上一节我们介绍了如何从单个页面爬取数据。本节中,我们来看看如何系统地访问多个页面。观察目标网站可以发现,不同页面的URL遵循一个简单的模式:基础URL后附加 `/page/{页码}`。

例如:
- 第2页: `https://example.com/page/2`
- 第3页: `https://example.com/page/3`

我们的目标是爬取前5页的数据。以下是构建这5个URL列表的方法:
```r
# 定义要爬取的页码
page_nos <- 1:5

# 使用glue包动态生成完整的URL列表
base_url <- "https://example.com/page/"
urls <- glue("{base_url}{page_nos}")
```
运行上述代码后,`urls` 变量将包含5个完整的URL,分别对应第1到第5页。
---
## 编写可复用的爬取函数
为了对每个URL执行相同的爬取操作,避免代码重复,最佳实践是编写一个函数。这个函数将接收一个URL作为输入,执行爬取操作,并返回一个包含该页面产品信息的数据框。
我们将这个函数命名为 `scrape_items`。其内部逻辑与我们之前编写的单页爬取代码类似,但被封装成了一个独立的单元。
```r
# 定义爬取单个页面的函数
scrape_items <- function(url) {
# 1. 读取网页内容
page <- read_html(url)
# 2. 提取产品标题
titles <- page %>%
html_elements(".product-title-class") %>% # 请替换为实际CSS选择器
html_text2()
# 3. 提取产品链接
links <- page %>%
html_elements(".product-link-class a") %>% # 请替换为实际CSS选择器
html_attr("href")
# 4. 提取产品价格
prices <- page %>%
html_elements(".price-class") %>% # 请替换为实际CSS选择器
html_text2() %>%
parse_number() # 将文本价格转换为数字
# 5. 将提取的信息组合成一个数据框(tibble)并返回
tibble(
title = titles,
link = links,
price = prices
)
}
```
**代码解释**:
- `function(url)`: 这定义了一个名为 `scrape_items` 的函数,它接受一个参数 `url`。
- 函数体内的步骤与单页爬取完全一致:读取HTML、提取元素、清理数据、组合成表。
- 函数最后返回一个 `tibble`,无需在函数内部将其赋值给一个变量。
定义函数后,我们可以对其进行测试,确保它在单个URL上工作正常。
```r
# 测试函数:爬取第一页
test_page1 <- scrape_items(urls[1])
print(test_page1)
```
---
## 将函数映射到所有URL
手动对每个URL调用函数是低效的。我们需要一种方法,能自动将 `scrape_items` 函数应用到 `urls` 列表中的每一个元素上。在R中,我们可以使用 `purrr` 包中的 `map` 函数来实现这种迭代操作。


`map` 函数的基本思想是:对列表中的每个元素应用指定的函数。
```r
# 使用map函数迭代爬取所有页面
all_items_list <- map(urls, scrape_items)
# 将列表中的多个数据框合并成一个大的数据框
all_items <- list_rbind(all_items_list)
```
**代码解释**:
- `map(urls, scrape_items)`: 这行代码对 `urls` 向量中的每个URL执行 `scrape_items` 函数,结果是一个列表,其中每个元素都是一个包含单页数据的数据框。
- `list_rbind()`: 这个函数将这个列表中的所有数据框按行(row-wise)合并,生成一个最终的统一数据框 `all_items`。
现在,`all_items` 数据框包含了从前5个页面爬取的所有产品信息(约80件商品)。
---
## 保存与分析爬取的数据
数据爬取完成后,我们应该将其保存到本地文件,以便后续分析,并避免因重复爬取而给网站服务器带来不必要的负担。
```r
# 将数据保存为CSV文件
write_csv(all_items, "data/items_multipage.csv")
```
保存数据后,我们就可以像处理任何其他数据集一样对其进行分析。例如,计算所有产品的平均价格:
```r
# 计算平均价格
mean_price <- all_items %>%
summarise(avg_price = mean(price, na.rm = TRUE))
print(mean_price)
```
---
## 总结

本节课中我们一起学习了如何从多个网页爬取数据。我们首先通过观察URL规律构建了目标页面列表,然后编写了一个可复用的函数来封装单页爬取逻辑,最后使用 `map` 函数将这个过程自动化地应用到所有页面上,并将结果合并保存。这种方法不仅高效,而且代码结构清晰,易于维护和扩展。掌握这些技能后,你将能够应对更复杂、规模更大的网络数据抓取任务。
# 033:欢迎与课程概述

在本课程中,我们将学习数据科学伦理,探讨在应用R语言进行数据分析时,我们“是否应该”执行某些任务,而不仅仅是“如何”执行。
## 课程回顾与引入
上一节我们回顾了前两门课程的内容。本节中,我们将正式开启本课程的学习。
大家好,欢迎来到本课程。我是Minet Tiincaranddell,我将担任这门名为“Data Science with R”专项课程的讲师。
这是系列课程中的第三门课,主题是“数据科学伦理”。
在第一门课程中,我们通过《R for Data Science》一书中的图表,介绍了数据科学周期。
在第二门课程中,我们更深入地探讨了数据转换,正式介绍了通过重塑和连接数据来进行数据整理。然后,我们退后一步,学习了如何从CSV文件和Excel电子表格导入数据集,以及如何从网络上抓取非结构化数据,并在R中将其重组为数据框。
在本课程中,我们将暂时搁置“如何在R中执行任务”的问题,转而探讨“我们是否应该执行这项任务”。具体来说,我们将讨论数据科学伦理中的主题,例如数据科学结果的误传、数据隐私和算法偏见。


让我们开始吧。
# 034:虚假陈述

在本节课中,我们将探讨数据科学中常见的虚假陈述问题。虚假陈述可能是有意或无意的,它会导致对数据结果的误解。我们将通过具体实例,分析几种主要的虚假陈述方式,并学习如何识别和避免它们。
## 虚假陈述的几种方式
虚假陈述可以通过多种方式发生。以下是本视频将讨论的几种主要方式。
* **因果关系的误述**:当研究本身只能证明相关性时,却声称发现了因果关系。
* **坐标轴与尺度的扭曲**:通过调整图表坐标轴的起点或刻度,使数据呈现不同的故事。
* **空间地图的误导**:在涉及人口的问题上,使用地理面积而非人口密度来绘制地图,从而产生误导性结论。
* **不确定性的忽略**:在报告结果时忽略误差范围或不确定性,导致结论过于绝对。
## 因果关系的误述
上一节我们概述了虚假陈述的几种方式,本节中我们来看看第一种:因果关系的误述。
请看《时代》杂志对一项关于运动与癌症风险研究的报道。文章标题称“运动可将某些癌症风险降低20%”。这个说法乍一看似乎合理,因为我们常听说运动与健康益处相关。然而,20%是一个相当大的效应量,我们需要谨慎判断。
接下来,我们看看《洛杉矶时报》对同一研究的报道,标题是“研究表明,运动可降低13种癌症的风险”。这里的“研究表明”是一个在科研报道中常用的笼统短语,它可能意味着“研究提示”或“研究证明”,但具体含义并不明确,却为前面的陈述增添了权威性。
最后,让我们审视这项研究本身。原始研究的标题是“休闲时间体力活动与144万成年人中26种癌症风险的相关性”。首先,样本量很大,但请记住,大样本量并不意味着因果关系。如果我们想推断“运动降低癌症风险”这样的因果关系,需要进行随机对照试验,而非观察性研究。
在这项研究中,志愿者被问及过去一年的体力活动水平。研究发现,与运动量最低的10%人群相比,运动量最高的10%人群多种癌症的发病率较低。同时,研究也发现运动与其他13种癌症(如胰腺癌、卵巢癌、脑癌)没有关联。
对比新闻标题“运动可降低13种癌症的风险”或“运动可将某些癌症风险降低20%”,这些说法听起来比研究本身所陈述的(相关性)要确定和因果得多。简化研究发现对于大众媒体报道是必要且有益的,但像这样在简化过程中(无论有意或无意)插入因果性语言,而底层研究并不支持的情况,实在过于常见。
## 具有误导性的可视化
在讨论了因果误述之后,我们接下来看看另一种常见的虚假陈述方式:具有误导性的数据可视化,特别是与坐标轴和尺度相关的扭曲。
以下是两个图表,它们展示了如果布什减税政策在2019年到期会发生什么。这两个图表的区别在于,其中一个的Y轴从0开始,而另一个的Y轴起始值紧贴当时的数据点。在Y轴不从0开始的图表中,减税政策的变化看起来要极端得多。有些数据可视化纯粹主义者坚持每个坐标轴都必须从0开始。我并非其中之一,但我同意,如果坐标轴不从0开始,必须有充分的理由,并且应在图中予以说明。
再看另一个例子。请仔细观察这个图表,它有什么问题?你会如何修正?
如果你说X轴看起来不对劲,那就对了。“去年”、“上周”和“当前”之间的间隔不应该是相等的。下图展示了相同数据在正确尺度坐标轴上的样子,讲述了一个截然不同的故事。
还有一个例子。再次请你仔细观察,这个图表有什么问题?你会如何修正?
同样,如果你说X轴看起来不对劲,那就对了。日期没有按顺序排列。如果不仔细看,很容易忽略这一点。这些是2020年新冠疫情高峰期的病例数,递减的柱状图看起来是好事,直到你意识到它们并没有按时间顺序讲述故事。下图是按时间顺序排列的相同数据,再次呈现了完全不同的情况。
再看一个关于计划生育服务提供的图表,它显示提供的堕胎服务增加,而癌症筛查和预防服务减少。请再次仔细观察,这个图表有什么问题?你会如何修正?
下图展示了相同数字在正确尺度上的样子。是的,癌症筛查和预防服务有所减少,但这种减少并没有被堕胎数量的增加所超越。
以上每个例子都是结果和可视化的虚假陈述。本视频的目的不是判断这些是有意还是无意的误述,但我们都可以同意,即使数据正确,如果坐标轴和尺度选择不当,也很容易讲述一个误导性的故事。
## 地图中的误导
在探讨了坐标轴扭曲的问题后,本节我们来看看结果如何在地图中产生误导,特别是那些展示地理区域的地图。
你认识这张地图吗?它展示了什么?给你一点时间思考。
这是一张美国地图,轮廓线表示县,美国大约有3000个县。颜色表示在2016年大选中,该县多数选票预计会投给共和党总统候选人(红色)还是民主党总统候选人(蓝色)。
当时有一本书的封面名为《公民支持特朗普》,展示了同样的地图。它写着“公民支持特朗普”,但颜色实际上表示的是县级投票结果。事实上,如果我们看普选票的份额,选举结果会看起来非常不同,如下方的条形图所示。
我们也可以考虑对可视化进行其他调整。我们可以按州着色,或者更好的是,根据各州选举人票数(影响选举结果)调整州的大小来着色。如果你不熟悉美国总统选举,这个指标可能没有意义。即使你熟悉,它也不容易理解,尽管它是更好的表示方式。
因此,在考虑如何可视化数据并确保我们没有虚假陈述结果时,我们需要走另一条路线。我们追求简洁,但又不希望我们的可视化产生误导。
## 忽略不确定性
最后,让我们谈谈忽略不确定性的问题。这是西班牙国家报纸《国家报》的一项调查结果摘要,显示了参与者对“是否希望加泰罗尼亚成为独立国家”这一问题的回答。“是”和“否”的比例非常接近。报纸的头条写道“调查显示,加泰罗尼亚公众舆论转向反对独立”。但请注意图表底部的误差范围:2.95%。这意味着什么?
如果我们考虑这一点,并在调查估计的比例周围标出误差范围边界,情况就大不相同了。两者存在巨大的重叠。这绝对不是一个我们可以总结为从一种回答“转向”另一种回答的情况。
## 总结与推荐
本节课中,我们一起学习了几种数据科学中常见的虚假陈述方式,包括因果关系的误述、坐标轴与尺度的扭曲、空间地图的误导以及不确定性的忽略。希望这些例子能帮助你在遇到数据科学结果时保持批判性思维,并在制作自己的可视化和摘要时做出正确的决策。


如果你有兴趣查看更多例子,我强烈推荐两本书:阿尔贝托·开罗的《How Charts Lie》(本视频中的一些例子来源于此),以及卡尔·伯格斯特龙和杰文·韦斯特的《Calling Bullshit》。这两本书都信息丰富且有趣,希望你能找时间深入阅读。
# 035:代码实践-行业与服务 📊


在本节课中,我们将学习如何识别并修正一个在社交媒体上分享的、存在数据可视化问题的图表。我们将使用R语言和`tidyverse`包来处理数据,并创建一个更准确、更清晰的改进版本。
## 概述与问题识别
首先,我们来看一下原始图表。该图表基于一项英国调查,受访者被问及他们认为铁路、水务、能源和皇家邮政等服务应由私营部门还是公共部门运营。


图表使用蓝色代表公共部门,红色代表私营部门,灰色代表“不知道”。虽然图表上显示的百分比数据是准确的,但其视觉呈现存在严重问题。
上一节我们介绍了图表背景,本节中我们来看看具体的问题所在。
图表中,每个服务对应的条形图被分割成三段,分别代表三种回答的比例。然而,这些分段的长度与其所代表的百分比并不匹配。例如,在“能源”服务中,公共部门支持率为87%,但蓝色分段看起来却不足条形图的一半。这表明**条形图分段的尺度与百分比数据不匹配**。
这种误导性的视觉呈现,如果不仔细看数字,会让人误以为公众对公共部门运营这些服务的支持率很低。我们的目标就是纠正这个问题。
## 数据准备与处理
为了分析和改进,我们首先需要获取并整理数据。数据以CSV文件提供,每一行代表一位受访者,每一列代表其对一项服务的回答。
以下是数据处理的步骤:
1. **加载必要的库并读取数据**:我们使用`tidyverse`进行数据整理和可视化。
```r
library(tidyverse)
survey_data <- read_csv("survey_data.csv")
```
2. **转换数据格式**:原始数据是“宽格式”,每个服务占一列。为了便于计算百分比,我们需要将其转换为“长格式”,即创建一个`service`列(服务名称)和一个`sector`列(回答选项)。
```r
survey_longer <- survey_data %>%
pivot_longer(cols = -ID,
names_to = "service",
values_to = "sector")
```
3. **计算百分比**:接下来,我们按服务分组,计算每个回答选项(公共、私营、不知道)的比例和百分比。
```r
survey_summary <- survey_longer %>%
count(service, sector) %>%
group_by(service) %>%
mutate(percentage = round((n / sum(n)) * 100, 0))
```
运行此代码可以验证,计算出的百分比(例如能源:公共87%,私营6%,不知道7%)与原始图表上的数字完全一致。
## 创建改进版可视化
现在我们已经有了正确的数据摘要,可以开始创建改进版的图表了。改进版的目标是:创建一个分段条形图,确保分段的长度精确反映百分比,同时保持与原始图表相似的图例位置、标题和说明文字。
以下是创建可视化图形的步骤:
1. **调整服务顺序**:首先,我们需要确保Y轴上的服务顺序与原始图表一致:皇家邮政、能源、水务、铁路。
```r
survey_longer <- survey_longer %>%
mutate(service = factor(service,
levels = c("Royal Mail", "Energy", "Water", "Rail")))
```
2. **绘制基础图形**:使用`ggplot2`创建分段条形图,将`service`映射到Y轴,用`sector`填充颜色。
```r
p <- ggplot(survey_longer, aes(y = service, fill = sector)) +
geom_bar(position = "fill")
```


3. **自定义颜色**:根据原始图表的信息,手动设置填充颜色:蓝色代表公共部门,红色代表私营部门,灰色代表不知道。
```r
p <- p +
scale_fill_manual(values = c("Don‘t know" = "gray",
"Private sector" = "red",
"Public sector" = "blue"))
```
4. **调整图例顺序**:原始图例的顺序是“公共部门”、“私营部门”、“不知道”。我们需要反转图例项的默认顺序。
```r
p <- p +
guides(fill = guide_legend(reverse = TRUE))
```
5. **添加标签与标题**:添加图表标题、说明文字,并移除坐标轴标签。
```r
p <- p +
labs(title = "Do you think the following services should be run in the private sector or the public sector?",
caption = "Base: All respondents. Unweighted total: 1858",
x = NULL,
y = NULL,
fill = NULL) # 移除图例标题
```
6. **调整主题与图例位置**:使用简洁的主题,并将图例放置在底部。
```r
p <- p +
theme_minimal() +
theme(legend.position = "bottom")
```
运行以上所有代码,即可生成改进后的可视化图表。
## 总结与对比
本节课中我们一起学习了如何批判性地评估数据可视化,并动手改进一个有缺陷的图表。
我们首先**识别了原始图表的问题**:条形图分段的视觉长度未能准确反映其代表的百分比,导致了严重的误导。

接着,我们**处理了数据**:将数据从宽格式转换为长格式,并计算了每个服务下各回答选项的准确百分比。

最后,我们**使用`ggplot2`重新创建了图表**,通过调整因子顺序、手动设置颜色、反转图例顺序、添加标签和应用主题,确保最终的可视化结果能够**准确地用分段宽度反映数据比例**。

改进后的图表清晰地表明,公众对于这些服务由公共部门运营仍然有很强的支持度,这与原始图表给人的第一印象截然不同。通过这个实践,我们巩固了数据整理、转换以及创建准确、有效可视化的技能。
# 036:数据隐私 🔒

在本节课程中,我们将探讨数据隐私这一重要议题。我们将了解个人数据如何被收集、使用和共享,并思考在数据分析工作中如何尊重和保护隐私。
---
## 数据收集无处不在
每次我们使用应用程序、网站和设备时,我们的数据都在被收集、使用或出售给他人。更重要的是,执法机构、金融机构和政府会基于这些数据做出直接影响人们生活的决策。
请思考一下,你今天在互联网上留下了哪些数据片段。回顾你登录过的所有账户、点击过的内容、主动或自动签到过的地点。这些行为可能都在追踪你。
你是否知道这些数据存储在哪里?谁可以访问它们?它们是否被分享给他人?如果你不知道这些问题的答案,你是否对此感到安心?
---
## 数据共享的边界
接下来,我们思考一下共享数据的问题。你愿意分享哪些信息?
以下是人们可能被要求或无意中分享的数据类型列表:
* 你的姓名、年龄、电子邮件和电话号码。
* 你观看过的每一个视频列表。
* 你评论过的每一个视频列表。
* 你的打字方式,例如速度和准确度。
* 你在不同内容上花费的时间。
* 你所有的私信列表,包括日期、时间和收件人。
* 关于你照片的信息,包括拍摄方式、地点(可能包含GPS坐标)和时间。
这个列表似乎越来越令人不安。
---
## 广告个性化背后的逻辑
让我们尝试思考另一个问题:你是否想过为什么会在谷歌上看到某个广告?
你可以暂停视频,去谷歌搜索一下,尝试弄清楚你是否开启了广告个性化功能,以及你的广告是如何被个性化的。然后回来继续学习。
这里还有一个反思性问题:你愿意你的浏览历史被用于以下哪些目的?
* 为你提供定向广告。😊
* 作为求职候选人的评分依据。
* 为投票目的预测你的种族和民族。
---
## 社交媒体数据的流向
假设你在一个社交媒体网站上创建了个人资料,并在个人资料中分享了你的个人信息。谁有权使用这些数据?
* 社交媒体公司本身及其关联公司。这在当今时代几乎是默认的。
* 社交媒体公司出售数据的公司。这一点我们现在也已经习以为常。
* 以科学为名的研究人员。这听起来似乎没有威胁性,对吗?
坦率地说,我很想说“是的,可以”,但让我们来看看这个关于OkCupid的案例研究。
---
## 案例研究:OkCupid数据泄露
OkCupid是最早的约会平台之一。在2016年,研究人员发布了7万名OkCupid用户的数据,包括用户名、政治倾向、吸毒情况和私密的性行为细节。
这些数据是用户自愿提供给OkCupid公司用于创建个人资料的。尽管这些研究人员没有公布OkCupid用户的真实姓名和照片,但他们可能没有意识到,用户的身份很容易从提供的细节中被识别出来。
例如,你是否在多个地方使用同一个用户名?是否存在一定数量的关于你的特征,可以让别人帮助你识别出你?
这是一位研究人员与一位访问过该数据集的人的在线对话截图。对方说:“这个数据集的可重识别性很高。它甚至包含了用户名。有没有做过任何匿名化处理?”研究人员回答:“没有,这些数据已经是公开的。”
你同意研究人员的观点吗?他们说:“有些人可能会反对收集和发布这些数据的伦理问题。然而,数据集中发现的所有数据都已经或曾经是公开可用的。因此,发布这个数据集只是以一种更有用的形式呈现它。”但“更有用”也可能意味着“可以恢复”,即实际上可以识别出具体的人。
---
## 如何在分析中保护隐私
那么,在分析个人在特定平台(例如他们的社交媒体资料)上自愿公开分享的数据时,如何确保不侵犯合理的隐私期望?
遗憾的是,我无法在这里给你具体的答案。但我鼓励你,在处理自己的数据以及使用他人作为数据源时,都要思考这个问题。
---
## 总结


本节课中,我们一起学习了数据隐私的基本概念。我们认识到个人数据在数字时代被广泛收集和利用,探讨了数据共享可能涉及的敏感信息,并通过OkCupid的案例看到了即使“公开”数据也可能带来隐私风险。最重要的是,我们开始思考作为数据分析者,应如何在工作中秉持伦理,尊重和保护数据主体的隐私。这是一个没有标准答案但至关重要的问题。
# 037:算法偏见



在本节课中,我们将探讨数据科学伦理中的另一个重要考量:算法偏见。我们将了解偏见如何通过数据进入算法,并影响翻译、招聘、面部识别和司法判决等多个领域。理解这些偏见是构建公平、负责任的数据科学应用的第一步。
---
## 垃圾进,垃圾出
在统计学中,有一个基本原则:**垃圾进,垃圾出**。这意味着,如果你的数据质量不高——这里“高质量”指的是**随机且具有代表性**——那么你的分析结果将是不可靠且无法推广的。
由此可以推导出一个推论:**偏见进,偏见出**。如果输入的数据本身带有偏见,那么算法输出的结果也会延续甚至放大这种偏见。
---
## 算法偏见与性别
上一节我们介绍了偏见的基本原理,本节中我们来看看算法偏见在性别方面的具体表现。
以下是谷歌翻译从土耳其语(一种没有性别化第三人称代词的语言)翻译成英语的例子:
* **原句**:`O bir doktor.`(意为:这个人是一名医生。)
* **旧版翻译**:`He's a doctor.`(他是一名医生。)
* **原句**:`O bir öğretmen.`(意为:这个人是一名老师。)
* **旧版翻译**:`She's a teacher.`(她是一名老师。)
这里存在一个模式:某些职业被自动分配了男性代词,而另一些则被分配了女性代词。谷歌这种带有性别偏见的翻译,其根源很可能在于其训练所使用的网络数据。算法只是延续了它在有偏见的数据中所看到的模式。
> 谷歌后来已经在绝大多数翻译案例中解决了这个问题。如今从土耳其语翻译成英语时,通常会使用中性表达“They are a doctor/teacher”。
另一个例子是亚马逊的实验性招聘算法。该系统为求职者打分(1到5星),但它并非以性别中立的方式评估软件开发等职位的候选人。由于这些职位上现有员工多为男性,算法“自学”认为男性候选人更可取。进一步调查还发现,支撑模型判断的数据存在问题,导致不合格的候选人经常被推荐给各种职位。
---
## 算法偏见与种族
接下来,让我们将目光转向算法偏见与种族的问题。
第一个例子是关于面部识别技术。研究人员发现,商用人脸识别软件中的人工智能系统存在性别和肤色偏见。一项面部分析软件的测试显示,其对浅肤色男性的错误率仅为 **0.8%**,而对深肤色女性的错误率高达 **34.7%**。事实上,一家主要美国科技公司声称其面部识别软件准确率超过 **97%**,但用于评估其性能的数据集中,**超过77%是男性,超过83%是白人**,根本不具代表性。
不幸的是,种族在刑事判决中也扮演着重要角色,即使判决得到了所谓“公正”算法的支持。2016年,ProPublica撰写了一篇关于用于评估被告再犯罪风险算法的深度报道。
以下是该算法预测的一些案例:
* **高风险**:多为黑人被告。
* **低风险**:多为白人被告。
ProPublica对佛罗里达州布劳沃德县7000多名被捕者的风险评分及其随后两年是否再犯罪的分析显示:
* 在预测会实施暴力犯罪的人中,有20%确实再犯。
* 当考虑所有罪行(包括轻罪)时,算法的准确率为61%。
* 此外,算法**错误地将黑人被告标记为未来犯罪者的可能性几乎是白人被告的两倍**。
* 白人被告被错误标记为低风险的情况也比黑人被告更常见。
那么,一个没有明确使用种族作为输入的算法,为何会表现出种族偏见?请记住我们的核心原则:**偏见进,偏见出**。算法的训练数据(如历史逮捕记录)可能本身就反映了社会中的系统性偏见,算法只是学习并复制了这些模式。
---
## 一个伦理困境:预测种族的软件
现在,我们来看一个引发伦理思考的具体案例。研究人员开发了一种方法,旨在通过聚合选举结果和种族构成来推断不同种族群体的投票行为。
他们提出的方法是通过选民登记记录来预测个人层面的种族信息,以减少聚合偏差。其核心是应用**贝叶斯定理**,将人口普查局的姓氏列表与地理编码的选民登记记录中的各种信息结合起来。
> **公式/方法核心**:`P(Race | Surname, Location) ∝ P(Surname | Race) * P(Race | Location)`
更重要的是,他们提供了一个开源软件包(R语言中的`wru`包)来实现这个方法。这固然很棒,但它也意味着**任何人都可以仅凭他人的姓氏(和地理位置)来预测其种族**。
这引发了一系列伦理问题:
* 这个算法是否可能被用于不当目的?
* 安装和使用这个软件包是否存在伦理顾虑?
* 发表这个模型本身是否符合伦理?代码的开源性是否改变了你的答案?
* 使用这个软件是否合乎伦理?你的答案是否会因其用途不同而改变?
无论个人答案如何,了解其存在和运作方式是重要的。该软件包附带一个示例姓名列表,并仅基于这些姓名来预测个体的种族。
---
## 总结与延伸阅读
本节课中,我们一起学习了算法偏见的概念及其在现实世界中的多种表现形式。我们看到,从翻译服务到招聘工具,从面部识别到司法判决,算法中的偏见可能对人们的生活产生深远影响,有时甚至会加剧社会不平等。
希望这节课能让你意识到偏见潜入算法的各种途径。如果你有兴趣深入探讨这个主题,强烈推荐以下阅读材料:
* ProPublica 的文章《机器偏见》。
* 书籍《**Ethics and Data Science**》。
* 书籍《**Weapons of Math Destruction: How Big Data Increases Inequality and Threatens Democracy**》。
* 书籍《**Algorithms of Oppression: How Search Engines Reinforce Racism**》。
在你的数据科学学习之旅中,终将学到一些可能被不道德使用的工具。你也可能因为商业目标、求知欲,或仅仅是上司的要求,而面临伦理上的抉择。

因此,希望你不断自问:如何训练自己做出正确的决策,或至少减少无意中做出错误决定的可能性?本课程希望能提供一些帮助,但这只是我们需要共同完成的庞大工作中的一小部分。
# 038:建模与推断入门


大家好,欢迎来到本课程。我是Mina Tiinica Rde,我将担任“R语言数据科学”专项课程中“建模与推断”部分的讲师。
在本节课中,我们将概述整个专项课程的脉络,并重点介绍即将开始的建模与推断课程的核心学习目标。我们将回顾之前课程的内容,并明确本课程将涵盖的关键主题,包括模型拟合、解释、评估以及统计推断。
## 课程回顾与定位
在专项课程的第一门课中,我们借助《R for Data Science》一书中的图表,介绍了数据科学的基本流程。我们重点学习了**数据可视化**与**数据转换**。
上一节我们回顾了数据探索与转换,本节中我们来看看数据整理。在第二门课程中,我们引入了**数据整理**的概念,这涉及数据的**重塑**、**合并**,以及将数据导入R环境。
随后,我们暂时搁置了“如何在R中执行任务”的技术探讨,转而思考“我们是否应该执行这项任务”,这门课程聚焦于**数据科学伦理**。具体而言,我们讨论了数据科学结果的**误传**、**数据隐私**以及**算法偏见**等问题。
## 本课程核心内容
在回顾了数据处理与伦理基础后,现在我们将正式进入建模领域。在本课程中,我们将深入探讨**建模**。
以下是本课程将涵盖的主要模块:
* **模型技术**:学习如何**拟合**、**解释**、**选择**和**评估**统计模型。
* **预测与分类**:探讨如何进行**预测**和**分类**,并评估我们预测与分类结果的**准确性**。
* **统计推断**:最后,学习如何从样本数据对目标总体进行**推断**,以及如何量化我们基于样本所得估计值的**不确定性**。
让我们开始学习吧。


本节课中,我们一起回顾了R语言数据科学专项课程的学习路径,明确了建模与推断课程在整个体系中的位置。我们简要重温了数据可视化、转换、整理及伦理等前期知识,并概述了本课程即将深入讲解的模型构建、评估与统计推断三大核心模块。接下来的课程将逐一展开这些内容。
# 039:模型语言



在本节课中,我们将深入理解什么是模型、模型如何工作,以及我们如何使用模型来理解数据。我们将从线性模型入手,学习核心概念和术语,并通过具体示例展示模型在解释关系和进行预测中的应用。
## 什么是模型?
我们使用模型来解释变量之间的关系并进行预测。模型是一种数学方法,用于描述一个事物如何影响另一个事物,或一个事物如何与另一个事物相关联。
在本课程中,我们主要关注**线性模型**。这些模型用直线来描述关系。但请记住,这只是开始,世界上还存在许多其他类型的模型。
以下是两种关系的示例:
* **线性关系**:数据点大致遵循直线模式。
* **非线性关系**:数据模式呈现曲线或弯曲。
本课程将聚焦于线性关系。了解线性模型为学习非线性模型奠定了坚实的基础。
## 一个建模示例
上一节我们介绍了模型的基本概念,本节中我们来看看一个具体的建模过程,并引入相关术语。
我们将使用一个具体的数据集进行建模:经典的 `mtcars` 数据集,它记录了汽车的燃油效率。
以下散点图展示了汽车重量(X轴)与其燃油效率(Y轴,单位:英里每加仑,MPG)之间的关系。

请思考两个问题:
1. 汽车重量与其燃油效率之间的关系是什么?
2. 如果一辆车重3500磅,你对其MPG的最佳猜测是多少?
观察散点图,每个点代表一辆车。现在,让我们回答第一个问题。
### 识别关系
当我们为数据添加一条趋势线时,关系变得清晰。

我们可以看到一种**负向的线性关系**:随着重量增加,每加仑英里数减少。这符合直觉:更重的汽车通常每加仑行驶里程更少,因此消耗更多燃料。这条线帮助我们看到了数据的整体模式,尽管个别汽车可能偏离这一趋势。
### 进行预测
现在回答第二个问题:如何预测一辆重3500磅汽车的MPG?
这正是我们模型的预测能力所在。我们可以使用趋势线进行预测。从X轴的3.5(代表3500磅)向上画一条垂直线与回归线相交,然后从该交点向左画一条水平线与Y轴相交,我们得到预测值:大约**18.5 MPG**。
这展示了模型如何让我们根据观察数据中出现的趋势,对未直接观测到的值进行预测。
## 核心模型术语
我们已经看到了模型的应用,现在来建立讨论模型时使用的关键词汇。这些术语对于理解模型如何工作至关重要。
以下是模型的核心组成部分:
* **结果变量**:这是我们试图理解其行为的变量,位于Y轴。也称为响应变量或因变量。
* **预测变量**:这是我们用来解释结果变量变化的变量,位于X轴。也称为解释变量或自变量。
* **模型函数**:即我们的回归线本身,用于进行预测。它由一个**截距**和每个预测变量的**斜率**组成。
* **预测值**:我们的模型函数给出的值,是基于预测变量值得出的结果变量的典型或期望值。
* **残差**:衡量实际观测值与其预测值之间的距离。计算公式为:`残差 = 观测值 - 预测值`。它告诉我们每个案例高于或低于期望值的程度。
## 在数据中应用术语
让我们将这些概念应用到我们的燃油效率数据中。
* **预测变量**:汽车的**重量**。我们用它来预测燃油效率。
* **结果变量**:**每加仑英里数(MPG)**。这是我们试图预测或解释的变量。
* **模型函数**:描述重量与MPG之间关系的**回归线**。这条线代表了我们对MPG如何随重量变化的最佳估计。
### 斜率和截距
* **斜率**:回归线的斜率告诉我们,预测变量每增加一个单位,结果变量会变化多少。在本例中,斜率为负,意味着重量每增加一千磅,我们预期MPG会减少一定量。
* **截距**:回归线与Y轴相交的点。它是当预测变量等于0时的预测值。在我们的汽车例子中,这代表重量为0的汽车的预测MPG。虽然这可能没有实际意义,但它是定义回归线所必需的数学组成部分。
### 相关性
相关性衡量两个变量之间线性关系的强度和方向。这些汽车的重量与燃油效率之间的**相关性为 -0.87**。
相关性范围从-1到1:
* 接近-1的值表示强烈的负相关关系。
* 接近1的值表示强烈的正相关关系。
* 接近0的值表示几乎没有线性关系。
我们的相关性-0.87表明重量与燃油效率之间存在强烈的负相关关系。请注意,相关性始终与回归线斜率的符号相同。
## 可视化模型与残差
以下是叠加在数据上的模型可视化。线条总结了整体关系,而各个点显示了实际数据以及模型周围存在多少变异性。

要创建此图,我们使用 `geom_point()` 函数添加点,然后使用 `geom_smooth()` 添加另一个图层来叠加回归线,其中 `method` 参数设置为 `"lm"`(线性模型的缩写)。
### 理解残差
残差对于理解模型对数据的拟合程度至关重要。在下图中,点根据其残差的正负进行着色。
* **正残差**(粉色):观测值高于预测值的案例。这些点位于回归线上方。
* **负残差**(黄色):观测值低于预测值的案例。这些点位于回归线下方。
残差的大小告诉我们模型的性能如何。较小的残差表示更好的预测。
## 模型的注意事项与用途
### 避免外推
将回归线延伸到数据范围之外需要谨慎。虽然数学上我们可以向两个方向无限延伸直线,但出于预测目的这样做可能是危险的。我们的模型基于观察到的数据,在此范围之外进行预测(称为**外推**)假设相同的关系继续成立,这可能不成立。
例如,在我们的案例中,不可能有重量小于零甚至只有几磅的汽车。对于远大于6000磅的汽车,或者对于超大汽车与常规尺寸汽车,重量与燃油效率之间的关系也可能不同。
因此,对于数据范围之外的预测应始终保持谨慎,除非有非常充分的理由或使用专门为外推(如基于过去时间序列数据预测未来)设计的更高级模型,否则应避免外推。
### 模型的优势与局限
模型有其优势和局限性。
* **优势**:模型可以揭示仅从数据中不易立即看出的模式。它们提供了一种系统化的方式来理解关系和进行预测。
* **局限性**:模型存在强加实际上不存在的结构的风险。模型是现实的简化,我们应始终对其结论保持健康的怀疑态度。
我们模型周围的变异(即我们讨论过的残差)与模型本身同样重要,甚至更重要。毕竟,统计学就是在未解释的背景下解释变异。我们线条周围的散点表明,可能还有其他影响燃油效率的因素我们尚未考虑,或者随机性可能起了很大作用。有时,向模型中添加更多解释变量或预测变量可以减少散点并改进我们的预测。
### 模型的两种主要用途
模型主要有两种用途:
1. **预测或分类**:我们输入预测变量的值,以获得结果变量的预测值。
2. **描述**:我们使用模型中的斜率来量化预测变量与结果变量之间的关系。

**预测/分类实例**:自动驾驶汽车如何判断前方的物体是人、另一辆车还是垃圾桶?它们使用基于数千张图像训练的模型进行分类。在线购物网站如何决定向你展示哪个广告?它们使用基于你的浏览历史和其他因素预测你最可能购买什么的模型。但关键问题是:如果其中任何一个出错了怎么办?后果可能从令人烦恼到危及生命,这就是理解模型准确性和局限性如此重要的原因。
**描述用途实例**:一项关于休闲、通勤、体力活动与血压的研究,检查了超过125,000名成年人中不同类型体力活动与血压水平之间的关系。

研究发现,较高水平的通勤和休闲时间体力活动(而非职业体力活动)与较低的血压和降低的高血压风险相关。这种关系是剂量依赖性的:活动越多,益处越大。例如,与不活动相比,最高水平的通勤和休闲活动组合与收缩压降低2.9毫米汞柱相关。
有趣的是,这种关系是年龄依赖性的。在老年人中关联更强:60岁以上的人显示收缩压降低4.64毫米汞柱,而40岁以下的成年人仅降低1.67毫米汞柱。这展示了模型如何帮助我们描述和量化复杂现实世界数据中的关系,使研究人员不仅能理解关系是否存在,还能了解其强度以及在不同群体中如何变化。
## 总结
本节课中我们一起学习了模型的基本语言,从预测变量和结果变量等基本词汇,到相关性和残差等概念。我们看到了模型如何服务于两个关键目的:进行预测和描述关系。请记住,模型是理解数据的强大工具,但它们也是现实的简化。始终要思考你的模型告诉了你什么,以及它没有捕捉到什么。
# 040:使用数值预测变量的线性回归 📊



在本节课中,我们将学习如何使用单个数值预测变量来构建线性回归模型。我们将通过一个具体的例子——电影评分数据,来演示如何拟合模型并解释其结果。
## 概述与工具准备 🛠️

在开始之前,我们需要准备分析工具。本次分析将使用三个关键的R包:
* **`fivethirtyeight`**:提供我们将要使用的电影评分数据。
* **`tidyverse`**:提供强大的数据处理和可视化工具。
* **`tidymodels`**:提供一套一致的统计建模框架。
## 数据介绍与探索 🎬
我们的数据来自FiveThirtyEight对电影评分的分析。我们将重点关注影评人评分与一个名为“烂番茄”的在线电影评分网站上的观众评分之间的关系。
为了使分析更清晰,我们将数据集的列名重命名为更直观的名称:`critics`代表影评人评分,`audience`代表观众评分。同时,我们将数据集重命名为`movie_scores`以便操作。
让我们查看一下数据。可以看到两个主要变量的结构:`critics`和`audience`。每一行代表一部不同的电影,我们可以看到前几部电影的评分范围。请注意,这两个变量都是数值型的。
## 数据可视化 📈
在开始建模之前,我们先通过可视化来理解我们试图建模的关系。
这张散点图以影评人评分(X轴)和观众评分(Y轴)展示了数据。我们将使用影评人评分来预测观众评分,因为影评人评分通常发布得更早,并且可以作为电影票房表现的指标。当然,它们也可能直接影响观众评分,但在此案例中,我们并非评估这两个变量之间的因果关系,而仅仅是关联。
图中的每个点代表一部电影。我们可以看到一种普遍的积极关系:影评人评分高的电影,观众评分也往往较高。然而,这种趋势周围也存在相当多的分散点,这表明关系并非特别强。
这个可视化帮助我们理解数据,并告诉我们预期的结果(例如,斜率为正),然后我们才开始构建模型。
## 理解回归模型 📐
现在,让我们正式定义回归模型。
回归模型是描述结果变量Y与预测变量X之间关系的函数。我们可以将这种关系写为:**y = 模型 + 误差**。模型部分(此处表示为 **f(x)**)代表我们试图捕捉的系统性关系。这也写作 **μ(y|x)**,代表给定x值时y的期望值。误差项 **ε** 代表我们的模型无法解释的y的部分,即随机性或模型中未包含的其他因素的影响。
从视觉上看,蓝线代表我们的模型,即x和y之间的系统性关系。每个独立的点代表一个实际观测值,每个点与线之间的垂直距离代表该观测值的误差或残差。模型为我们提供了任何给定x值时y的期望值,但个体观测值会围绕这个期望值变化。
## 简单线性回归模型 📉
我们将使用简单线性回归模型来建模数值型结果Y与单个数值型预测变量X之间的关系。
简单线性回归的方程为:**y = β₀ + β₁x + ε**。
* **β₁** 代表关系的真实斜率,即x每增加一个单位,y的变化量。
* **β₀** 代表真实截距,即当x等于0时y的期望值。
* **ε** 代表误差项,即我们的线性模型无法解释的y的部分。
这些β值代表总体中真实但未知的参数。当然,我们不知道真实的β值,我们必须从数据中估计它们。
当我们估计这些参数时,使用不同的符号:**ŷ = b₀ + b₁x**。这里,**b₁** 是我们的估计斜率,**b₀** 是我们的估计截距。注意,这个方程中没有误差项,因为 **ŷ** 代表我们的预测值,而不是实际观测值。预测值与实际观测值之间的差异给出了我们的残差。
## 最小二乘法与最佳拟合线 🔍
那么,我们如何为斜率和截距选择最佳值呢?我们可以在数据中画出许多不同的线。蓝线代表我们认为可能是最佳拟合的线,但灰线显示了其他可能的关系。有些线比另一些线更符合数据。我们需要一个系统的方法来确定哪条线是最好的,这就是最小二乘法的用武之地。
让我们关注残差,即观测值与预测值之间的差异。在这张图中,蓝色垂直线显示了每个数据点的残差。有些是正的(观测值高于预测线),有些是负的(观测值低于预测线)。
最小二乘法为我们提供了一种系统的方法来找到最佳拟合线。对于每个观测值i,残差表示为 **eᵢ**,它等于 **yᵢ**(结果的观测值)减去 **ŷᵢ**(结果的预测值)。残差平方和是 **e₁² + e₂² + ... + eₙ²**。
最小二乘线就是使这个量(残差平方和)最小化的那条线。我们将残差平方以消除负号,这样正负残差就不会相互抵消,并且对幅度较大的残差赋予更高的权重。这种方法是最常用的确定最佳拟合线的方法。
## 拟合模型与解释结果 🎯
现在,让我们将模型实际拟合到电影评分数据中。我们将使用`tidymodels`框架,因此首先用`linear_reg()`指定模型。然后使用`fit()`函数拟合模型,并提供模型公式,格式为`结果 ~ 预测变量`,即`audience ~ critics`,以及变量来源的数据框名称`movie_scores`。
我们将模型对象存储为`movies_fit`。然后可以使用`tidy()`函数打印模型摘要,其中显示了斜率、截距以及它们的标准误、检验统计量和p值。
我们可以看到,估计的截距 **b₀** 是32.3,估计的斜率 **b₁** 是0.519。

## 解释斜率和截距 📝
让我们确保能正确解释斜率和截距。
斜率是关于预测变量每增加一个单位时结果变化率的描述。截距是当预测变量等于0时结果的预测值。尽管这些描述听起来相对简单,但在数据和研究问题的背景下考虑这些数字的含义时,很容易出错。
让我们看一下斜率。模型的斜率是0.519。哪个解释是最好的?正确答案是选项B:**影评人评分每增加1分,我们预计观众评分平均会高出0.519分**。
这个解释很重要,因为它包含了“我们预计”和“平均”这两个短语,承认了这是我们预计平均会发生的情况,而不是每部电影都会完全遵循这种模式。
让我们写出完整的模型方程:**预测观众评分 = 32.3 + 0.519 × 影评人评分**。
斜率表明,影评人评分每增加1分,我们预计观众评分平均会高出0.519分。
截距的解释表明,对于影评人评分为0的电影,我们预计观众评分平均为32.3分。
请注意,斜率告诉我们变量之间的关系,而截距告诉我们当预测变量等于0时的预期结果。
这就引出了一个重要问题:在这个背景下,我们的截距有意义吗?当预测变量可以合理地取值为0或接近0,或者我们在0附近有观测数据时,截距是有意义的。在我们的电影例子中,影评人评分为0在理论上是可能的,它代表了一部影评人非常不喜欢的电影。因此,我们的截距32.3表明,即使是影评人评分最差的电影,仍可能获得中等的观众评分。
然而,如果你的预测变量实际上不可能为0,请小心,截距可能没有有意义的解释。
## 最小二乘回归的关键性质总结 📚
最后,让我们回顾一下最小二乘回归的关键性质。
最小二乘回归线最小化了残差平方和。首先,回归线总是通过质心点,即对应于x平均值和y平均值的坐标点。这意味着,如果我们将x的平均值(x̄)代入回归方程,y的预测值将是y的平均值(ȳ)。通过变换,这个性质可以用来求解截距:**b₀ = ȳ - b₁x̄**。请注意,你永远不需要手动计算这些,但回归到均值的这一性质仍然很重要。
其次,直线的斜率与相关系数的符号相同。关系是:**b₁ = r × (sᵧ / sₓ)**,其中r是相关系数,sᵧ是y的标准差,sₓ是x的标准差。再次强调,你不需要手动计算这些值,但了解这些公式有助于在看到R的回归输出时进行快速的心理检查。
第三,残差之和始终为0。这意味着正残差和负残差相互抵消。这就是为什么在确定最佳拟合线时,我们不最小化残差之和,而是最小化残差平方和。
最后,残差和x值必须不相关。残差的大小和x值之间不应存在系统性关系。这些性质确保我们的最小二乘线不仅在数学上是最优的,而且具有良好的统计特性。
## 总结 🎓


在本节课中,我们一起学习了如何使用单个数值预测变量构建简单线性回归模型。我们从数据准备和可视化开始,理解了回归模型的基本概念,然后使用最小二乘法拟合了模型,并详细解释了斜率和截距的含义。最后,我们回顾了最小二乘回归的关键性质。通过电影评分的具体案例,你现在应该能够理解、拟合并解释一个简单的线性回归模型了。
# 041:鱼类建模 🐟


在本节课中,我们将使用鱼类数据集进行建模实践,重点关注两种常见的市场销售鱼种。我们将使用 `tidyverse` 包进行数据处理和可视化,并使用 `tidymodels` 包进行建模。
## 加载数据与初步探索
首先,我们运行代码单元来加载所需的包。
```r
library(tidyverse)
library(tidymodels)
```

我们将使用的数据来自Kaggle,这是一个流行的机器学习和预测建模竞赛平台。让我们从本课程的GitHub仓库读取数据并查看其结构。
```r
fish <- read_csv("https://raw.githubusercontent.com/your_repo/fish.csv")
```
查看数据有多种方式。我们可以点击环境面板中的数据对象,或者将其打印到控制台。数据包含55个观测值和7个列。`species` 列告诉我们鱼的种类,其他列则是鱼的身体测量值。
以下是数据字典:
* `species`:鱼的种类(分类变量)。
* `weight`:重量(克)。
* `vertical_length`:垂直长度(厘米)。
* `diagonal_length`:对角线长度(厘米)。
* `cross_length`:交叉长度(厘米)。
* `height`:高度(厘米)。
* `diagonal_width`:对角线宽度(厘米)。
我们的目标是研究鱼的重量与高度之间的关系,即根据高度预测重量。
## 可视化关系
首先,我们创建一个合适的图形来研究这种关系,并确保为其添加适当的标签。
```r
ggplot(data = fish, aes(x = height, y = weight)) +
geom_point() +
labs(title = "鱼类重量与高度关系图",
x = "高度 (厘米)",
y = "重量 (克)")
```

这段代码创建了一个散点图,其中高度在x轴,重量在y轴。我们可以看到一个正相关关系,并且数据点似乎分成了两个簇,这可能与鱼的种类有关。


为了更清晰地量化这种关系,我们可以在图上添加一条最佳拟合直线。


```r
ggplot(data = fish, aes(x = height, y = weight)) +
geom_point() +
geom_smooth(method = "lm", se = FALSE, color = "lightgray") +
labs(title = "鱼类重量与高度关系图(含线性拟合)",
x = "高度 (厘米)",
y = "重量 (克)")
```
参数 `method = "lm"` 表示使用线性模型进行平滑,`se = FALSE` 表示不显示置信区间。
这个图可以帮助我们回答许多问题,例如:鱼的重量和高度之间是否存在关系?这种关系是什么?我们还可以利用这条线进行预测。
## 基于图形进行预测
现在,让我们尝试仅通过观察图形来预测高度为10、15和20厘米的鱼的重量。
* 对于高度10厘米,预测重量约为375克。
* 对于高度15厘米,预测重量约为600克。
* 对于高度20厘米,预测重量约为975克。
**残差** 是观测值与模型预测值之间的差异,在图形上通常表现为数据点到拟合线的垂直距离。
## 拟合线性模型
接下来,我们正式使用R来拟合一个线性模型。
```r
fish_height_weight_fit <- linear_reg() %>%
fit(weight ~ height, data = fish)
tidy(fish_height_weight_fit)
```
`linear_reg()` 函数指定了模型类型,`fit()` 函数执行拟合。公式 `weight ~ height` 表示用 `height` 预测 `weight`。我们将模型对象保存为 `fish_height_weight_fit`,并使用 `tidy()` 函数以整洁的格式查看模型摘要,其中包括截距和斜率的估计值。
根据输出,我们的模型公式为:
**$$\widehat{weight} = -288 + 60.9 \times height$$**
现在,我们可以使用这个公式进行精确计算,预测高度为10、15和20厘米的鱼的重量。
```r
new_heights <- c(10, 15, 20)
predicted_weights <- -288 + 60.9 * new_heights
predicted_weights
```
## 计算并可视化残差
为了计算数据集中所有鱼的预测值和残差,我们使用 `augment()` 函数。
```r
fish_height_weight_aug <- augment(fish_height_weight_fit, new_data = fish)
ggplot(data = fish_height_weight_aug, aes(x = height)) +
geom_point(aes(y = weight)) +
geom_smooth(aes(y = weight), method = "lm", se = FALSE, color = "lightgray") +
geom_segment(aes(xend = height, y = weight, yend = .fitted), color = "red", alpha = 0.6) +
labs(title = "线性模型拟合与残差可视化",
x = "高度 (厘米)",
y = "重量 (克)")
```
`augment()` 函数将原始数据与模型的预测值(`.fitted`)和残差(`.resid`)合并。我们使用 `geom_segment()` 绘制从每个观测点到其对应预测点的红色线段,这些线段直观地展示了残差。
## 提取模型参数
有时我们需要从模型输出中提取特定数值(如斜率),以便在报告或文档中动态引用。
```r
slope <- tidy(fish_height_weight_fit) %>%
filter(term == "height") %>%
pull(estimate) %>%
round(1)
# 在R Markdown中,我们可以使用内联代码 `r slope` 来输出这个值。
```
## 评估相关性
**相关系数** 衡量两个数值变量之间线性关系的强度和方向,其值介于-1(完全负相关)到1(完全正相关)之间。我们可以直接计算高度和重量的相关系数。
```r
fish %>%
summarize(r = cor(height, weight))
```
计算出的相关系数约为0.954,表明高度和重量之间存在非常强的正线性关系。
## 考虑物种的影响
之前我们注意到数据点可能按物种聚类。现在,我们分别为 `Bream` 和 `Roach` 这两种鱼绘制单独的拟合线。
```r
fish_filtered <- fish %>% filter(species %in% c("Bream", "Roach"))
ggplot(data = fish_filtered, aes(x = height, y = weight, color = species, shape = species)) +
geom_point() +
geom_smooth(method = "lm", se = FALSE) +
labs(title = "按物种分组的重量与高度关系",
x = "高度 (厘米)",
y = "重量 (克)")
```
通过按物种着色并分组拟合,我们可以看到每条拟合线对其对应物种的数据拟合得更紧密。这说明在预测重量时,同时考虑高度和物种可能会得到更好的模型,这为后续学习包含多个预测变量的模型奠定了基础。
## 尝试非线性拟合
最后,我们探索一下线性模型之外的拟合方法。例如,使用局部加权回归(LOESS)来拟合一条平滑曲线。
```r
ggplot(data = fish, aes(x = height, y = weight)) +
geom_point() +
geom_smooth(method = "loess", se = FALSE) +
labs(title = "使用LOESS方法的非线性拟合",
x = "高度 (厘米)",
y = "重量 (克)")
```
将 `method` 参数改为 `"loess"` 后,拟合线变得弯曲,更能捕捉数据中的非线性模式。这提醒我们,除了线性模型,还有许多其他类型的模型可供选择。
## 总结
在本节课中,我们一起学习了:
1. **探索性数据分析**:使用 `ggplot2` 可视化两个数值变量(鱼的高度和重量)之间的关系。
2. **简单线性回归**:使用 `tidymodels` 的 `linear_reg()` 和 `fit()` 函数拟合模型,理解截距和斜率的含义。
3. **预测与残差**:根据模型进行预测,并理解残差是观测值与预测值之差。
4. **模型诊断**:使用 `augment()` 获取预测值和残差,并通过可视化残差来评估模型拟合情况。
5. **相关性**:计算并解释相关系数,以量化线性关系的强度。
6. **引入分类变量**:探索了在分析中考虑第三个变量(如物种)的影响,为多元回归做准备。
7. **模型灵活性**:简要尝试了非线性拟合方法(LOESS),了解了模型的多样性。

通过这些步骤,你实践了从数据探索、模型拟合、结果解释到简单诊断的完整建模流程。
# 042:使用分类预测变量的线性回归 📊


在本节课中,我们将学习如何使用线性回归模型,但这次我们的预测变量是分类变量(例如岛屿名称),而不是数值变量。我们将通过一个关于企鹅体重的实际案例,来理解如何解释模型结果并进行预测。

## 数据准备与探索
首先,我们需要加载必要的R包并查看数据。我们将使用 `palmerpenguins` 包中的企鹅数据集,以及 `tidyverse` 和 `tidymodels` 包进行数据处理和建模。
```r
# 加载必要的包
library(palmerpenguins)
library(tidyverse)
library(tidymodels)
```
我们使用的数据集包含了来自三个岛屿(Bisco、Dream 和 Togerson)的344只南极企鹅的身体测量数据。变量包括体重、喙长、喙深、鳍肢长度以及它们被观察到的岛屿。
我们的研究问题是:探究企鹅的体重与其被记录时所在的岛屿之间是否存在关系。
与之前不同,我们的预测变量 `island` 是分类变量。这意味着我们不能将其放在一个数值尺度上衡量(例如,不能说Bisco比Dream“多”或“少”)。因此,我们需要调整建模思路。
在建模之前,我们先通过可视化来探索数据。由于预测变量是分类的,散点图并不适用。箱线图或小提琴图是展示数值变量(体重)在分类变量(岛屿)各水平上分布的理想选择。
以下是绘制箱线图的代码:
```r
# 绘制体重与岛屿关系的箱线图
ggplot(penguins, aes(x = island, y = body_mass_g)) +
geom_boxplot() +
labs(title = "不同岛屿企鹅的体重分布",
x = "岛屿",
y = "体重 (克)")
```
从图中我们可以观察到一些初步模式:Bisco岛的企鹅平均体重似乎更重,而Dream岛和Togerson岛的企鹅体重较低且较为相似。
## 拟合线性回归模型
接下来,我们拟合一个线性回归模型来量化这种关系。语法与我们之前使用数值预测变量时非常相似。
```r
# 拟合线性回归模型:用岛屿预测体重
penguin_model <- linear_reg() %>%
fit(body_mass_g ~ island, data = penguins)
# 查看模型摘要
tidy(penguin_model)
```
模型摘要会显示一些有趣且不同于之前的结果。尽管我们只使用了一个预测变量 `island`,但输出中却出现了两个斜率项:`islandDream` 和 `islandTorgersen`,而 `islandBisco` 不见了。
这是因为当模型包含分类预测变量时,R会自动将其转换为**虚拟变量**。对于一个有K个水平的分类变量,我们只需要K-1个虚拟变量。被省略的那个水平(此处是Bisco)被称为**基线水平**。
## 理解虚拟变量与模型解释
对于我们的三个岛屿,R在幕后创建了两个虚拟变量:
* `islandDream`: 当企鹅来自Dream岛时为1,否则为0。
* `islandTorgersen`: 当企鹅来自Torgersen岛时为1,否则为0。
当这两个虚拟变量都为0时,就代表企鹅来自基线水平——Bisco岛。
现在,让我们来解释模型系数。假设我们的模型方程是:
**`预测体重 = 4716 - 1003 * islandDream - 1010 * islandTorgersen`**
* **截距 (4716)**: 代表基线组(Bisco岛企鹅)的**预测平均体重**,即4716克。
* **斜率 islandDream (-1003)**: 代表Dream岛企鹅与Bisco岛企鹅的**预测平均体重差异**。Dream岛企鹅平均比Bisco岛企鹅轻1003克。
* **斜率 islandTorgersen (-1010)**: 代表Torgersen岛企鹅与Bisco岛企鹅的**预测平均体重差异**。Torgersen岛企鹅平均比Bisco岛企鹅轻1010克。
每个斜率系数都描述了特定组与基线组之间的平均差异。
## 使用模型进行预测
我们可以使用模型方程手动计算每个岛屿的预测平均体重。
* **Bisco岛**: 两个虚拟变量均为0。
`预测体重 = 4716 - 1003*0 - 1010*0 = 4716克`
* **Dream岛**: `islandDream` 为1,`islandTorgersen` 为0。
`预测体重 = 4716 - 1003*1 - 1010*0 = 3713克`
* **Torgersen岛**: `islandDream` 为0,`islandTorgersen` 为1。
`预测体重 = 4716 - 1003*0 - 1010*1 = 3706克`
我们也可以使用R代码自动进行预测,这在变量很多时更为方便。
```r
# 创建一个包含各岛屿一只企鹅的新数据框
new_penguins <- tibble(island = c("Bisco", "Dream", "Torgersen"))
# 使用模型进行预测
augment(penguin_model, new_data = new_penguins)
```
预测结果将与我们的手动计算一致:Bisco岛为4716克,Dream岛为3713克,Torgersen岛为3706克。
## 总结
本节课中,我们一起学习了如何使用分类预测变量进行线性回归建模。关键要点如下:
1. **虚拟变量编码**:当模型包含分类预测变量时,R会自动将其水平编码为虚拟变量。对于有K个水平的变量,会创建K-1个虚拟变量。
2. **基线水平**:第一个水平(或按字母顺序排列的第一个)通常被设为基线。截距表示基线组的预测结果值。
3. **系数解释**:每个斜率系数表示对应分类水平与基线水平之间预测结果的平均差异。这与数值预测变量中“单位变化导致的结果变化”的解释不同。


通过本案例,我们掌握了处理分类预测变量的基本方法,这是数据科学建模中一项非常重要的技能。
# 043:线性回归中的异常值


在本节课程中,我们将学习线性回归中的异常值。我们将探讨什么是异常值,它们如何影响回归模型,以及如何识别和处理它们。
## 概述
线性回归模型假设数据点大致围绕一条直线分布。然而,实际数据中常会出现一些远离主体数据云的点,这些点被称为异常值。它们可能出现在X方向、Y方向或两个方向上。理解这些点对模型的影响至关重要。
## 异常值的类型与影响
上一节我们介绍了线性回归的基本概念,本节中我们来看看异常值如何具体影响回归线。并非所有在单变量分布上极端的点,在双变量模型中都会成为异常值。如果一个点虽然在一个变量上取值极端,但它符合两个变量之间的整体趋势,那么它在回归模型中可能不会显得异常。
以下是三种常见的异常值情况,通过散点图可以更好地理解:
* **图A**:该点在Y方向上极端偏低,且不遵循整体模式。它会对回归线产生轻微影响。
* **图B**:该点在X和Y方向上都远离主体数据,但它恰好落在回归线附近。因此,它对回归线几乎没有影响,无论是否包含它,最佳拟合线都几乎相同。
* **图C**:该点在X和Y方向上都远离主体数据,且不符合整体趋势。它会将回归线向上拉,导致对大多数数据点的拟合变差。这是一个**有影响力的点**。
每个图表下方的残差图提供了另一个视角,显示了每个点距离模型预测值的远近。
## 更多示例分析
接下来,我们通过另一组三个例子来加深理解。
* **图D**:在右侧出现一小簇点。它们不仅仅是异常值,而是形成了一个次要的数据云,会强烈地将回归线拉向自身。
* **图E**:右侧有一个单独的点,它完全决定了回归线的斜率,在主数据本身没有趋势的地方制造出存在趋势的假象。
* **图F**:异常点在X和Y方向上都远离主体,但它恰好落在已有的趋势线上。它具有高杠杆值,但并未影响模型。
## 核心概念与术语
现在我们已经看了一些例子,让我们来明确相关术语。
* **异常值**:指在任何方向上与主体数据分离的点。
* **杠杆点**:指在水平方向上远离主体数据云中心的点。它们倾向于对回归线施加更大的“拉力”,因此被称为具有**高杠杆**的点。
* **有影响力的点**:指那些实际会影响回归模型(如斜率或截距)的点。
> 核心概念:并非所有异常值都有影响力,但许多有影响力的点都是具有高杠杆的异常值。
判断一个点是否有影响力的标准是:如果移除该点,回归线的斜率或位置会发生显著变化。
## 处理异常值的实用建议
以下是处理数据中异常值的一些实用建模建议:
* **对比分析**:始终用包含和不包含疑似异常值的数据分别测试你的分析。比较并讨论异常值对模型拟合的影响。
* **呈现选择**:将两种模型结果都呈现给利益相关者,以便选择最合理的解释。
* **谨慎删除**:除非有强有力的、可辩护的理由(如数据录入错误),否则应非常谨慎地删除异常值。随意删除不寻常的点可能适得其反,导致模型过于乐观、遗漏关键情况,甚至支持误导性结论。
始终将异常值视为信息,而不仅仅是噪声。异常值可能很微妙、难以处理,有时甚至具有误导性,但理解它们如何影响你的模型是进行可靠统计建模的关键部分。
因此,要始终思考:这个点试图告诉我什么?
## 总结

本节课中,我们一起学习了线性回归中异常值的概念。我们区分了异常值、杠杆点和有影响力的点,并通过示例观察了它们对回归线的不同影响。最后,我们掌握了处理异常值的实用原则:通过对比分析来评估其影响,并谨慎对待删除操作,将异常值视为潜在的重要信息。
R语言数据科学:07_02_02:多预测变量的线性回归


在本节课中,我们将要学习如何构建包含多个预测变量的线性回归模型。我们将从一个简单的单变量模型开始,逐步引入第二个预测变量,并学习如何解释模型结果、比较模型优劣以及进行可视化。
在开始分析之前,我们需要加载必要的R包。本次分析将使用三个关键包:
* `DAAG`:用于获取数据集。
* `tidyverse`:用于数据处理和可视化。
* `tidymodels`:用于建模。
我们将分析 `allbacks` 数据框,它包含了从澳大利亚国立大学J.H. Madonald教授书架上收集的15本书的测量数据。每本书有四个关键特征:
* `volume`:体积,以立方厘米为单位。
* `area`:面积,以平方厘米为单位(本教程中不会过多使用此变量)。
* `weight`:重量,以克为单位。
* `cover`:封面类型,精装(HB)或平装(PB)。
上一节我们介绍了数据集,本节中我们来看看第一个模型:一个仅使用体积来预测重量的简单线性回归。
我们使用 `linear_reg()` 函数来指定模型,然后使用 `fit()` 函数来拟合模型。模型公式为 `weight ~ volume`,并指定数据框为 `allbacks`。我们将模型对象存储为 `allbacks_fit_1`,然后使用 `tidy()` 函数来显示模型摘要。
```r
allbacks_fit_1 <- linear_reg() %>%
fit(weight ~ volume, data = allbacks)
tidy(allbacks_fit_1)
```
观察模型输出,我们可以看到斜率系数为 **0.709**。这意味着,体积每增加一立方厘米,模型预测书的重量平均增加 **0.709** 克。
散点图清晰地显示了体积与重量之间的正相关关系,这符合直觉:更大的书往往更重。
我们知道,书的体积并不是影响其重量的唯一因素。书的封面类型(平装或精装)也会对重量产生影响。
因此,让我们通过添加封面类型作为预测变量来增强我们的模型。这就得到了一个多元线性回归模型。代码中唯一的改变是在 `fit()` 函数中,我们现在使用加号运算符将 `cover` 添加到模型公式中:`weight ~ volume + cover`。
```r
allbacks_fit_2 <- linear_reg() %>%
fit(weight ~ volume + cover, data = allbacks)
tidy(allbacks_fit_2)
```
请注意,这不仅增加了一个新的斜率系数,也略微改变了原有的系数。体积系数现在是 **0.718**,而封面系数是 **-184**。同时,由于 `cover` 是一个分类变量,R自动创建了一个名为 `coverPB` 的虚拟变量(哑变量)来表示平装书。按字母顺序排在首位的水平(精装HB)被设定为基线水平,因此不会在模型输出中单独显示。我们稍后会解释 **-184** 这个数字的含义。
现在,让我们仔细解读多元线性回归模型的系数。
* **体积的斜率 0.718**:在保持封面类型不变的情况下,书的体积每增加一立方厘米,模型预测其重量平均增加 **0.718** 克。
* **封面(平装)的斜率 -184**:在保持体积不变的情况下,模型预测平装书比精装书平均轻 **184** 克。
* **截距 198**:这表示体积为零的精装书的预测重量,这在现实中不具实际意义。
“保持其他变量不变”是一个新的概念,只要你使用包含多个预测变量的模型,这个概念就会一直存在。它的意思是,系数估计值告诉我们,在**其他预测变量保持不变**的情况下,当给定的预测变量发生变化时,结果变量的预测变化量。
在书的重量的例子中,这个假设是合理的:一本书的重量可以随着其体积的增加而增加,同时不改变其封面类型(毕竟,现实中存在体积很大的平装书)。然而,在某些自然现象中,这可能是一个非常不现实的假设。例如,很难在不改变湿度的情况下改变降雨量。因此,当我们使用包含独立预测变量的线性模型时,需要考虑我们所做的这些简化假设在现实中是否可行,或者是否需要考虑预测变量之间的交互作用。这将是下一个视频的主题。现在,让我们先专注于这个模型。
到目前为止,我们拟合了两个模型:一个仅用体积预测重量,另一个用体积和封面类型预测重量。我们如何知道哪个模型更好呢?
让我们检查 **R平方**,它衡量了模型解释结果变量(重量)变异性的百分比。
* 我们的第一个模型(仅使用体积)解释了约 **80.3%** 的书重变异性(来自 `glance(allbacks_fit_1)` 输出中的 `r.squared` 值)。
* 我们的第二个模型(添加了封面类型)解释了约 **92.7%** 的重量变异性。这是一个显著的提升。
然而,有一个重要的注意事项:**当我们向模型添加预测变量时,R平方总是会增加**,无论它们是否真正有帮助。这时就需要引入**调整后R平方**。
调整后R平方会对拥有额外预测变量的模型进行惩罚。其背后的思想是,如果你使解释变异性的方式变得更复杂,你就会受到惩罚。如果调整后R平方值仍然更高,意味着即使有惩罚,R平方的增益也足够高,这说明新的预测变量确实有意义地为模型增加了预测能力,值得保留。因此,我们说**调整后R平方是一个更客观、因而也是更好的统计量,用于比较具有不同数量预测变量的模型**。
* 第一个模型的调整后R平方是 **0.787**。
* 第二个模型(增加了预测变量)的调整后R平方要高得多,为 **0.915**。
因此,对于预测书的重量,**模型2更优**。
在结束之前,让我们将我们的模型可视化。
在模型1中,我们看到数据点和一条穿过它们的单一回归线。这条线捕捉了总体的上升趋势,但请注意点是如何分散在直线周围的。这代表了模型中未解释的变异性。R平方为 **0.803**,意味着我们仅用体积作为预测变量就解释了约80%的书重变异。
我们的第二个模型讲述了一个更丰富的故事。我们现在有两条平行的回归线:橙色代表精装书,蓝色代表平装书。平行线表明,对于精装书和平装书,重量都随着体积的增加而类似地增加,但平装书的重量始终更轻。请注意,与我们的第一个模型相比,数据点更紧密地聚集在它们各自的线周围。这种视觉上的改进与我们R平方值的增加是匹配的。
**非常重要的一点是**:我们并没有“发现”精装书和平装书的重量随体积增加的方式相似。相反,我们是通过拟合一个用两个独立变量预测重量的模型,**强制**了这种平行结构。


本节课中我们一起学习了多元线性回归模型的构建与解读。关键要点是:
1. 在解释多元回归模型的斜率系数时,需要说明是在**保持其他预测变量不变**的情况下,某个预测变量的变化所产生的影响。
2. **调整后R平方**是你在比较具有不同数量预测变量的模型时的好帮手,它帮助你在模型复杂度和解释力之间取得平衡。
# 045:主效应与交互效应 📊



在本节课中,我们将学习何时在模型中加入预测变量之间的交互效应,以及如何运用奥卡姆剃刀原则,在包含与不包含交互效应的竞争模型之间做出选择。
## 概述

在预测变量对结果的影响中,有时一个变量的影响会因另一个变量的水平不同而改变。这种关系被称为交互效应。我们将通过一个书籍重量预测的案例,探讨如何判断是否需要引入交互效应,并学习使用调整后的R平方作为客观的模型选择标准。
## 数据准备与问题定义
在开始分析之前,我们需要加载必要的R包。我们将使用三个包:`DAAG` 用于获取数据集,`tidyverse` 用于数据处理和可视化,`tidymodels` 用于建模。
```r
library(DAAG)
library(tidyverse)
library(tidymodels)
```
我们将再次使用 `allbacks` 数据集,它包含15本书的信息,目标是基于书的**体积**和**封面类型**来预测其**重量**。
在预测时,我们考虑两种可能的解释:
1. 体积与重量之间的关系不因封面类型而变化。
2. 体积与重量之间的关系会因封面类型而变化。
第一种情况对应着平行线的模型,第二种情况则对应着非平行线的模型。从视觉上看,哪个模型拟合得更好?这似乎有些主观。因此,在做出决定之前,我们需要更正式地分析。
## 模型选择原则:奥卡姆剃刀 🪒
我们的指导原则是**奥卡姆剃刀**。该原则指出,在预测能力相当的竞争性假设中,应选择假设最少的那一个。
在建模术语中,这意味着我们只应在新变量能为模型带来有价值的预测能力提升时,才将其加入模型。换句话说,我们更倾向于最简单、最好的模型,即**简约模型**。
现在,让我们重新审视这两个模型。根据奥卡姆剃刀原则,哪个模型更可取?两个模型看起来差异不大,非平行线的斜率也没有显著不同。两个模型似乎都能合理地拟合数据,但平行线模型更简单。除非交互效应模型能显著提高预测能力,否则我们应该选择更简单的主效应模型。
那么,我们能否找到一种方法来量化这个决策呢?在做出选择之前,我们先回顾一下如何拟合这些模型。
## 模型拟合与比较
### 模型1:主效应模型
模型1是我们之前见过的**主效应模型**,它只考虑体积和封面类型的主效应,而不考虑它们之间的交互作用。
以下是拟合该模型的步骤:
1. 使用 `linear_reg()` 指定模型。
2. 使用 `fit()` 函数拟合模型,预测变量之间用加号 `+` 连接。
3. 使用 `tidy()` 显示模型系数摘要。
4. 使用 `glance()` 显示模型级别的统计量,如R平方和调整后的R平方。
```r
# 拟合主效应模型
model_main <- linear_reg() %>%
fit(weight ~ volume + cover, data = allbacks)
# 查看系数
tidy(model_main)
# 查看模型整体统计量
glance(model_main)
```
### 模型2:交互效应模型
模型2在预测变量之间加入了**交互效应**,允许重量随体积变化的速率因封面类型而异。
拟合交互效应模型的步骤类似,但在模型公式中,我们使用星号 `*` 或显式地使用冒号 `:` 来添加交互项。R的语法很智能:当你使用 `*` 时,它会自动包含主效应。
```r
# 拟合交互效应模型(使用 * 自动包含主效应)
model_int <- linear_reg() %>%
fit(weight ~ volume * cover, data = allbacks)
# 查看系数
tidy(model_int)
# 查看模型整体统计量
glance(model_int)
```
**关于R语法的一个说明**:在统计建模中,如果你包含了交互效应,主效应也应该包含在模型中。即使你在 `fit()` 函数中疏忽或省略了它们,R仍然会将它们添加到模型中(正如从模型摘要中看到的那样)。虽然你可能会在其他地方看到以简化方式编写的交互效应模型代码,但在本课程中,我们将力求精确,始终在公式中明确写出主效应和交互效应。
## 做出选择:调整R平方
现在,到了决定是否包含交互效应的时候了。包含交互效应的模型拥有更多的预测变量。请记住,在比较具有不同数量预测变量的模型时,我们使用**调整后的R平方**进行模型选择。
虽然交互效应模型的R平方略高(93% 对 92.7%),但**调整后的R平方**对增加的交互效应施加了惩罚,其值并未更高。主效应模型的调整R平方为91.5%,而交互效应模型为91.05%。
这个结果与我们最初的视觉观察一致,并为我们提供了一个客观的衡量标准,解释了为何在本案例中,交互效应不值得被纳入预测变量之中。
## 总结

本节课中,我们一起学习了主效应与交互效应的概念。我们了解到,交互效应描述了一个预测变量对结果的影响如何随另一个预测变量的变化而变化。通过运用奥卡姆剃刀原则,我们倾向于选择更简约的模型。在比较模型时,**调整后的R平方**是一个关键指标,因为它考虑了模型的复杂度,惩罚了不必要的额外参数。在我们的书籍重量预测案例中,尽管交互效应模型的普通R平方略高,但其调整R平方更低,这支持我们选择更简单的主效应模型。
# 046:贷款利率建模 📊


在本节课中,我们将通过一个关于贷款利率的实际数据集,练习使用多个预测变量进行建模。我们将学习如何准备数据、探索数据、拟合线性回归模型,并解释模型结果。
---
## 数据准备与探索
首先,我们需要加载必要的R包并准备数据。我们将使用 `tidyverse` 进行数据处理和可视化,使用 `tidymodels` 进行建模,并使用 `openintro` 包中的数据集。
```r
# 加载必要的包
library(tidyverse)
library(tidymodels)
library(openintro)
# 数据准备
loans <- loan_full_schema %>%
mutate(
credit_utilization = total_credit_utilized / total_credit_limit,
bankruptcy = as.factor(if_else(public_record_bankrupt == 0, 0, 1)),
verified_income = droplevels(verified_income),
homeownership = str_to_title(homeownership),
homeownership = factor(homeownership, levels = c("Rent", "Mortgage", "Own")),
credit_checks = inquiries_last_12m
) %>%
select(
interest_rate,
credit_utilization,
bankruptcy,
verified_income,
homeownership,
issue_month,
credit_checks,
debt_to_income,
total_credit_limit,
total_credit_utilized
)
```
以上代码执行了数据清洗和变量创建。我们创建了 `credit_utilization`(信用利用率)和 `bankruptcy`(破产记录)等新变量,并对分类变量进行了因子化处理,以便在建模中使用。
接下来,让我们初步探索数据,了解典型贷款利率和借款人的特征。
```r
# 查看典型贷款利率
summary(loans$interest_rate)
mean(loans$interest_rate)
median(loans$interest_rate)
min(loans$interest_rate)
max(loans$interest_rate)


# 可视化利率分布
ggplot(loans, aes(x = interest_rate)) +
geom_histogram(binwidth = 1, fill = "steelblue", color = "white") +
labs(title = "贷款利率分布", x = "利率 (%)", y = "计数")
```
探索结果显示,平均利率约为12.4%,中位数为12%,范围从5.31%到30.9%。这表明数据中存在较高的利率变异性。
---
## 单预测变量建模
上一节我们介绍了数据的基本情况,本节中我们来看看如何使用单个预测变量建立模型。
首先,我们尝试用信用利用率来预测贷款利率。
```r
# 拟合模型:利率 ~ 信用利用率
rate_u_fit <- linear_reg() %>%
fit(interest_rate ~ credit_utilization, data = loans)
# 查看模型摘要
tidy(rate_u_fit)
# 可视化模型
ggplot(loans, aes(x = credit_utilization, y = interest_rate)) +
geom_point(alpha = 0.3) +
geom_smooth(method = "lm", se = FALSE, color = "red") +
labs(title = "利率与信用利用率的关系", x = "信用利用率", y = "利率 (%)")
```
模型摘要显示,截距约为10.5,斜率约为4.73。这意味着信用利用率为0的借款人,其预测利率平均为10.5%。信用利用率每增加1个单位(即100%),预测利率平均增加4.73%。
---
## 分类预测变量建模
接下来,我们看看房屋所有权状态如何影响贷款利率。
```r
# 拟合模型:利率 ~ 房屋所有权
rate_home_fit <- linear_reg() %>%
fit(interest_rate ~ homeownership, data = loans)
# 查看模型摘要
tidy(rate_home_fit)
```
模型摘要中,截距(12.9%)代表租房者(基线水平)的平均预测利率。`homeownershipMortgage` 的系数为-0.866,表示有抵押贷款的借款人,其利率平均比租房者低0.866%。`homeownershipOwn` 的系数为-0.611,表示拥有房产的借款人,其利率平均比租房者低0.611%。
---
## 多预测变量建模(主效应)
现在,我们将信用利用率和房屋所有权状态同时纳入模型,但不考虑它们的交互作用。
```r
# 拟合模型:利率 ~ 信用利用率 + 房屋所有权
rate_u_home_fit <- linear_reg() %>%
fit(interest_rate ~ credit_utilization + homeownership, data = loans)
# 查看模型摘要
tidy(rate_u_home_fit)
```
根据模型结果,我们可以为不同房屋所有权状态的群体分别写出回归方程。
以下是针对不同群体的估计回归方程:
* **租房者 (Renters):**
`interest_rate_hat = 9.93 + 5.34 * credit_utilization`
* **抵押贷款者 (Mortgage):**
`interest_rate_hat = (9.93 - 0.696) + 5.34 * credit_utilization = 10.626 + 5.34 * credit_utilization`


* **房产拥有者 (Own):**
`interest_rate_hat = (9.93 - 0.128) + 5.34 * credit_utilization = 10.058 + 5.34 * credit_utilization`
在这个主效应模型中,不同群体的预测利率随信用利用率变化的**斜率是相同的**(均为5.34),但**截距不同**。
---
## 多预测变量建模(交互效应)
上一节我们假设信用利用率对利率的影响在不同房屋所有权群体中是相同的。本节中,我们允许这种影响存在差异,即引入交互效应。
```r
# 拟合模型:利率 ~ 信用利用率 * 房屋所有权 (包含交互项)
rate_u_home_int_fit <- linear_reg() %>%
fit(interest_rate ~ credit_utilization * homeownership, data = loans)
# 查看模型摘要
tidy(rate_u_home_int_fit)
```
现在,我们可以为每个群体写出包含不同斜率的回归方程。
以下是包含交互效应时,针对不同群体的估计回归方程:
* **租房者 (Renters):**
`interest_rate_hat = 9.44 + 6.20 * credit_utilization`
* **抵押贷款者 (Mortgage):**
`interest_rate_hat = (9.44 + 1.39) + (6.20 - 1.64) * credit_utilization = 10.83 + 4.56 * credit_utilization`
* **房产拥有者 (Own):**
`interest_rate_hat = (9.44 + 0.697) + (6.20 - 1.06) * credit_utilization = 10.137 + 5.14 * credit_utilization`
在这个交互效应模型中,不同群体的预测利率随信用利用率变化的**斜率和截距都不同**。
---
## 模型比较与选择
我们有了两个包含多个预测变量的模型:一个只有主效应,另一个包含交互效应。如何选择?
我们可以使用 `glance()` 函数查看模型级别的统计量,如 R² 和调整后 R²。
```r
# 比较两个模型
glance(rate_u_home_fit) # 主效应模型
glance(rate_u_home_int_fit) # 交互效应模型
```
* **R²** 表示模型解释的结果变量变异性的百分比。
* **调整后 R²** 对 R² 进行了调整,惩罚了模型中预测变量的数量。公式为:`调整后 R² = R² - 惩罚项`。
比较结果:
* 主效应模型的调整后 R² 约为 0.0679。
* 交互效应模型的调整后 R² 约为 0.0689。
交互效应模型的调整后 R² **略高**。虽然增幅很小,但考虑到它可能捕捉到了更真实的群体间差异,在此例中可能更受青睐。在实际分析中,还需要结合领域知识和研究目标进行判断。
最后,我们尝试在交互效应模型中加入 `issue_month`(贷款发放月份)变量。
```r
# 尝试添加新变量
rate_u_home_int_month_fit <- linear_reg() %>%
fit(interest_rate ~ credit_utilization * homeownership + issue_month, data = loans)
glance(rate_u_home_int_month_fit)
```
新模型的调整后 R² 下降至 0.0688。**调整后 R² 并未随着该预测变量的加入而增加**,因此不建议将其纳入最终模型。
---
## 总结

本节课中我们一起学习了如何使用 R 语言对贷款利率数据进行多预测变量建模。
我们首先进行了数据准备,创建了关键变量并处理了分类数据。接着,我们从单变量模型开始,逐步构建了包含多个预测变量及其交互项的模型。我们学会了如何解释连续变量和分类变量的系数,并为不同子群体写出具体的回归方程。最后,我们使用调整后 R² 作为标准,比较了不同复杂度模型的性能,并做出了模型选择。
通过这个完整的流程,你掌握了从数据探索到模型构建、解释和比较的基本技能,这是数据科学实践中至关重要的一环。
# 047:逻辑回归 📊



在本节课中,我们将要学习逻辑回归,它也被称为分类。我们将了解如何将线性回归的思想扩展到处理结果变量是二分类(例如是/否、成功/失败)的情况。
## 回顾已学的回归类型
在深入新概念之前,我们先快速回顾一下之前学过的回归类型。
### 简单线性回归
在简单线性回归中,我们建模一个数值型结果变量与一个数值型预测变量之间的关系。其核心公式为:
`Y = β₀ + β₁X + ε`
例如,我们可以用影评人的评分(X)来预测观众评分(Y)。
### 带有分类预测变量的多元线性回归
我们也可以将分类预测变量纳入模型。例如,预测婴儿出生体重时,可以将母亲的吸烟习惯(吸烟者=1,非吸烟者=0)作为一个预测变量。模型公式与简单线性回归类似,但X代表一个二元指示变量。
### 结合数值与分类预测变量
更进一步,我们可以在同一个模型中结合数值型和分类型预测变量。例如,用企鹅的鳍肢长度(数值)和所在岛屿(分类)来预测其体重。这可以是一个主效应模型(不同组有相同斜率但不同截距)或交互效应模型(允许斜率也不同)。
上一节我们回顾了处理数值型结果的线性模型,本节中我们来看看当结果变量本身是二分类时,情况会发生什么变化。
## 二分类结果与逻辑回归
当我们的结果变量不是数值型,而是二分类时,它只能取两个值:0或1。这可以代表是/否、成功/失败、真/假等。
观察一个散点图,你会发现Y值只有0和1,但它们与预测变量X之间存在明显关系:X值较高时,Y更可能为1;X值较低时,Y更可能为0。描述这种关系的典型曲线是一条S型曲线(或Sigmoid曲线),这正是逻辑回归的标志。
你可能会问,这为什么重要?因为如果我们能建模预测变量与二分类结果之间的关系,我们就能进行一项强大的操作:**分类**。我们不再预测精确的数值,而是预测一个新的观测属于哪个类别。
以下是几个现实世界的例子:
* **垃圾邮件过滤**:预测变量是邮件中的词频、字符特征等,结果是邮件是否为垃圾邮件(1=是,0=否)。
* **医疗诊断**:预测变量是医学影像的特征(如像素强度、纹理),结果是组织是否癌变(1=是,0=否)。这种分类的准确性至关重要。
* **金融风控**:银行使用逻辑回归评估贷款违约风险。输入申请人的财务和人口统计信息,模型输出其违约的概率。
需要注意的是,虽然数学模型相同,但这些应用引发了关于偏见、公平性和伦理的重要问题,我们必须始终予以考虑。
## 为何不能直接用线性回归?
如果我们尝试用一条直线去拟合二分类数据,会得到类似下图的结果。这条直线会预测出小于0和大于1的值,这对于概率来说没有意义。一条直线无法捕捉二分类结果的本质。
显然,我们需要一种不同于标准线性回归的方法。
## 逻辑回归模型原理
逻辑回归不直接建模Y,而是建模 **Y等于1的概率**。例如,给定一封新邮件,它是垃圾邮件的概率是多少?
这个S型曲线有特定的数学形式。概率P(Y=1)的公式为:
`P = e^(β₀ + β₁X) / [1 + e^(β₀ + β₁X)]`
其中,`β₀ + β₁X` 是我们熟悉的线性部分。
经过代数变换(我们不必手动计算),可以得到一个更简洁的形式,即**逻辑回归模型**:
`log[P / (1 - P)] = β₀ + β₁X`
让我们分解一下这些术语:
* **P**:概率,介于0和1之间。
* **P / (1-P)**:**优势比**,范围从0到正无穷。
* **log[P / (1-P)]**:**对数优势比**(或Logit),范围从负无穷到正无穷。
这样,通过Logit函数(链接函数)的转换,我们又将模型预测值映射到了一个线性空间(负无穷到正无穷)。这使得逻辑回归成为**广义线性模型**的一个例子。
我们使用**最大似然估计**来拟合模型参数(即找到最佳拟合数据的S曲线),得到带有估计系数 β̂₀ 和 β̂₁ 的方程。
## 从概率到分类的三步过程
一旦我们有了拟合模型,分类就成为一个三步过程:
以下是分类决策的三个步骤:
1. **选择阈值概率(P*)**:例如,设定 P* = 0.85。规则是:如果预测概率 ≤ P*,则预测 Y=0;如果预测概率 > P*,则预测 Y=1。
2. **找到决策边界(X*)**:求解S曲线与阈值概率线相交时对应的X值。这个X*就是分界点。在X*左侧的点被分类为0,右侧的点被分类为1。
3. **执行分类**:对于一个具有新值 X_new 的数据点,只需检查它位于决策边界的哪一侧,即可做出预测。
**阈值的选择至关重要**。降低阈值(例如从0.85降到0.15)会使决策边界左移,导致更多观测被分类为1。这个选择取决于对不同类型错误(如将垃圾邮件误判为正常邮件 vs. 将正常邮件误判为垃圾邮件)的成本考量。
## 多预测变量的逻辑回归
逻辑回归不限于单个预测变量。以下是使用两个数值预测变量和一个二分类结果的例子。我们可以看到两个不同的点簇(橙色代表Y=0,蓝色代表Y=1)。
对于多个预测变量,概率公式变得更复杂,但对数优势比仍然保持线性关系。我们只需在线性方程中为每个预测变量添加一项:
`log[P / (1 - P)] = β₀ + β₁X₁ + β₂X₂ + ... + βₖXₖ`
一个美妙之处在于,即使有多个预测变量,**决策边界仍然是线性的**。在二维情况下,它是一条分隔两个类别的直线;在三维情况下,是一个平面;维度更高时,则是一个超平面。
## 总结
本节课中我们一起学习了逻辑回归。我们看到了它如何通过建模概率而非原始结果,将线性回归扩展到处理二分类结果。我们探讨了从垃圾邮件检测到医疗诊断的实际应用,了解了其基于Logit函数和对数优势比的数学基础,并学习了三步分类过程。

核心洞见在于:通过使用正确的转换(Logit链接函数),我们可以将线性建模技术应用于分类问题。这使得逻辑回归成为最通用、应用最广泛的统计方法之一。
# 048:构建垃圾邮件过滤器 📧


在本节课中,我们将学习如何在R中拟合逻辑回归模型,用于处理二元响应变量,并将其应用于分类问题。我们将通过构建一个垃圾邮件过滤器来具体演示这一过程。
## 数据加载与概览
首先,我们需要加载必要的R包和数据。数据来源于David Diaz(OpenIntro教材作者之一)在2012年前三个月的收件箱邮件,所有个人身份信息均已移除。


我们将使用 `tidyverse` 包进行数据处理和可视化,使用 `tidymodels` 包进行建模,数据则来自 `openintro` 包。
```r
# 加载必要的包和数据
library(tidyverse)
library(tidymodels)
library(openintro)
# 加载邮件数据集
data(email)
# 查看数据结构
glimpse(email)
```
运行上述代码后,我们可以看到数据集包含近4000行和21列。我们的目标变量是 `spam`,它是一个因子变量,表示邮件是否为垃圾邮件。
## 探索性数据分析
我们的第一个目标是利用邮件中感叹号的数量来预测其是否为垃圾邮件。让我们从一些探索性数据分析开始。
### 密度图分析
首先,我们创建一个密度图来探究垃圾邮件与感叹号数量之间的关系。
```r
# 创建密度图
ggplot(email, aes(x = exclaim_mess, fill = spam)) +
geom_density(alpha = 0.5) +
facet_wrap(~spam) +
labs(title = "垃圾邮件与非垃圾邮件的感叹号数量分布",
x = "感叹号数量",
y = "密度")
```
通过分面图,我们可以更清晰地看到垃圾邮件和非垃圾邮件在感叹号数量上的分布差异,但初步看来,这似乎不是一个非常显著的区别特征。
### 汇总统计
接下来,我们计算垃圾邮件和非垃圾邮件中感叹号数量的平均值。
```r
# 计算分组平均值
email %>%
group_by(spam) %>%
summarise(mean_exclaim = mean(exclaim_mess, na.rm = TRUE))
```
结果显示,非垃圾邮件的平均感叹号数量低于垃圾邮件。这可能是因为垃圾邮件倾向于使用更夸张、紧急的语言。
## 线性回归模型的尝试(错误示范)
在上一节中,我们查看了数据的基本分布。本节中,我们首先尝试使用线性回归模型进行预测,尽管我们知道这可能不是最佳选择。
```r
# 尝试拟合线性模型(会导致错误)
lm_fit <- linear_reg() %>%
fit(spam ~ exclaim_mess, data = email)
# 错误:结果变量应为数值型,而非因子
```
由于 `spam` 是因子变量,线性模型无法直接拟合。为了演示,我们将其转换为数值型。
```r
# 将spam转换为数值型并拟合模型
email_numeric <- email %>%
mutate(spam_numeric = as.numeric(spam) - 1) # 将0/1因子转换为0/1数值
lm_fit <- linear_reg() %>%
fit(spam_numeric ~ exclaim_mess, data = email_numeric)
# 查看模型摘要
tidy(lm_fit)
```
虽然模型可以拟合,但当我们可视化结果时,会发现这条拟合直线在分类问题上没有实际意义。
```r
# 可视化线性模型拟合结果
ggplot(email_numeric, aes(x = exclaim_mess, y = spam_numeric)) +
geom_jitter(alpha = 0.2, height = 0.05) +
geom_smooth(method = "lm", se = FALSE) +
scale_y_continuous(breaks = c(0, 1)) +
labs(title = "线性模型拟合效果",
x = "感叹号数量",
y = "是否为垃圾邮件 (0=否, 1=是)")
```
图形显示,线性模型无法为分类提供有效的决策边界。
## 逻辑回归模型
既然线性模型不适用,本节我们将转向逻辑回归模型。逻辑回归通过建模邮件为垃圾邮件的概率(记为 \( p \))来进行分类。
我们首先定义**优势比(Odds)** 和**对数优势比(Log-Odds)**:
* 优势比: \( \text{Odds} = \frac{p}{1-p} \)
* 对数优势比: \( \log(\text{Odds}) = \log\left(\frac{p}{1-p}\right) \)
逻辑回归模型的形式为:
\[ \log\left(\frac{p}{1-p}\right) = \beta_0 + \beta_1 \times \text{exclaim\_mess} \]
通过求解 \( p \),我们可以得到概率预测公式:
\[ p = \frac{e^{\beta_0 + \beta_1 \times \text{exclaim\_mess}}}{1 + e^{\beta_0 + \beta_1 \times \text{exclaim\_mess}}} \]


### 拟合单预测变量模型
现在,让我们使用感叹号数量作为唯一预测变量来拟合逻辑回归模型。
```r
# 拟合逻辑回归模型
log_fit <- logistic_reg() %>%
fit(spam ~ exclaim_mess, data = email)
# 查看模型系数
tidy(log_fit)
```
模型给出了截距 \( \beta_0 \) 和斜率 \( \beta_1 \) 的估计值。根据这些值,我们的估计模型可以写作:
\[ \log\left(\frac{\hat{p}}{1-\hat{p}}\right) = -2.12 - 0.07 \times \text{exclaim\_mess} \]
### 进行预测
假设我们收到一封包含10个感叹号的邮件,我们想预测它是垃圾邮件的概率。

在数学上,我们将 `exclaim_mess = 10` 代入上述模型计算对数优势比,然后通过公式转换为概率 \( p \)。
在R中,我们可以使用 `predict()` 函数更简便地完成:

```r
# 1. 创建包含新观测的数据框
new_email <- tibble(exclaim_mess = 10)
# 2. 预测概率
predict(log_fit, new_data = new_email, type = "prob")
# 3. 预测类别(默认阈值为0.5)
predict(log_fit, new_data = new_email, type = "class")
```
输出显示,这封邮件被分类为“非垃圾邮件”(`0`),其对应的概率约为0.91。
### 关于决策阈值的思考
默认情况下,`predict()` 函数使用0.5作为分类阈值。但在实际应用中(如垃圾邮件过滤器),这个阈值可能需要调整。
* **降低阈值(如0.3)**:更敏感,能捕获更多垃圾邮件,但可能将更多正常邮件误判为垃圾邮件(假阳性率高)。
* **提高阈值(如0.7)**:更严格,能确保进入收件箱的邮件质量,但可能漏掉一些垃圾邮件(假阴性率高)。
最佳阈值取决于你对“误拦重要邮件”和“漏放垃圾邮件”两者代价的权衡。
## 评估模型性能
上一节我们学会了如何对单封邮件进行预测。本节中,我们将对整个数据集进行分类,并评估模型性能。
### 对整个数据集进行分类
我们可以使用 `augment()` 函数为原始数据集添加预测结果。
```r
# 为整个数据集添加预测
augmented_data <- augment(log_fit, new_data = email)
# 查看新增的列
glimpse(augmented_data)
# 新增列包括:.pred_class(预测类别), .pred_0(预测为非垃圾邮件的概率), .pred_1(预测为垃圾邮件的概率)
```
### 可视化预测结果
让我们可视化模型的预测情况。
```r
# 绘制预测结果
ggplot(augmented_data, aes(x = exclaim_mess, y = spam, color = .pred_class)) +
geom_jitter(alpha = 0.3, width = 0, height = 0.1) +
labs(title = "单变量逻辑回归模型预测结果",
x = "感叹号数量",
y = "真实类别",
color = "预测类别")
```
从图中可以看出,仅使用感叹号数量的模型将所有邮件都预测为了“非垃圾邮件”,这说明该单变量模型区分能力很弱。
### 拟合全变量模型
为了提高性能,我们尝试使用数据集中的所有变量来拟合一个新的逻辑回归模型。
```r
# 使用所有变量拟合逻辑回归模型
log_fit2 <- logistic_reg() %>%
fit(spam ~ ., data = email) # “.” 表示使用除响应变量外的所有其他变量
# 为数据集添加新模型的预测
augmented_data_full <- augment(log_fit2, new_data = email)
# 可视化全模型预测结果
ggplot(augmented_data_full, aes(x = exclaim_mess, y = spam, color = .pred_class)) +
geom_jitter(alpha = 0.3, width = 0, height = 0.1) +
labs(title = "全变量逻辑回归模型预测结果",
x = "感叹号数量",
y = "真实类别",
color = "预测类别")
```
使用更多变量后,模型开始能够将部分邮件识别为垃圾邮件。
### 计算错误率
最后,我们来量化模型的性能,计算假阳性率和假阴性率。
```r
# 创建混淆矩阵并计算错误率
confusion_matrix <- augmented_data_full %>%
count(spam, .pred_class) %>%
group_by(spam) %>%
mutate(rate = n / sum(n))
confusion_matrix
```
通过上表,我们可以计算出:
* **假阳性率**:非垃圾邮件被误判为垃圾邮件的比例。
* **假阴性率**:垃圾邮件被误判为非垃圾邮件的比例。
这些指标有助于我们全面评估分类器的表现,并指导后续的模型优化(如特征工程、调整阈值或尝试其他算法)。
## 总结
在本节课中,我们一起学习了:
1. **探索性数据分析**:通过可视化和汇总统计初步了解预测变量与结果之间的关系。
2. **线性回归的局限性**:认识到对于分类问题,线性回归在理论和实践上都不适用。
3. **逻辑回归原理**:理解了通过建模概率的对数优势比来进行分类的基本思想。
4. **在R中实现逻辑回归**:使用 `logistic_reg()` 和 `fit()` 函数拟合模型,使用 `predict()` 和 `augment()` 函数进行预测和结果评估。
5. **决策阈值**:讨论了分类阈值对模型敏感性和特异性的影响。
6. **模型评估**:通过混淆矩阵计算假阳性率和假阴性率,量化模型性能。

通过构建垃圾邮件过滤器的实例,我们掌握了应用逻辑回归解决二元分类问题的完整流程。
# 049:分类与决策错误


在本节课程中,我们将讨论分类问题以及可能出现的决策错误。我们将通过垃圾邮件检测的实际例子,探讨分类过程中可能出错的地方,以及如何量化模型在决策方面的性能。
## 分类决策的四种可能结果
上一节我们介绍了分类问题的基本概念。本节中,我们来看看当我们对一封邮件(例如一封声称您已获得专属奖励的推广邮件)进行分类决策时,可能出现的四种情况。
我们的目标是构建一个垃圾邮件过滤器模型,根据邮件的特征(如单词和字符数量,或更相关的特征如用词选择)将其分类为垃圾邮件或非垃圾邮件。
一旦我们做出决策或分类,可能会出现以下四种情况:
* **真阳性**:将一封确实是垃圾邮件的邮件标记为垃圾邮件。
* **真阴性**:将一封确实不是垃圾邮件的邮件标记为非垃圾邮件。
* **假阳性**:将一封不是垃圾邮件的邮件标记为垃圾邮件。这被称为**第一类错误**。
* **假阴性**:将一封是垃圾邮件的邮件标记为非垃圾邮件。这被称为**第二类错误**。
到目前为止,前两种情况没有错误。但有时,垃圾邮件会进入收件箱,或者真实邮件会意外进入垃圾邮件箱。我们当然希望永远不犯后两种错误,但可以想象,这通常是不可能的,或者至少不太可能。
## 错误率与性能指标
理解了决策的可能结果后,我们需要量化这些错误的程度。以下是评估分类模型性能的几个关键指标。
**假阴性率**是指一封垃圾邮件被标记为非垃圾邮件的概率。用公式表示为:
`假阴性率 = 假阴性数量 / (真阳性数量 + 假阴性数量)`
**假阳性率**是指一封非垃圾邮件被标记为垃圾邮件的概率。用公式表示为:
`假阳性率 = 假阳性数量 / (真阴性数量 + 假阳性数量)`
我们希望这两个比率都尽可能低。
另外两个重要的术语是**敏感度**和**特异度**。
* **敏感度**是假阴性率的补数。`敏感度 = 1 - 假阴性率`
* **特异度**是假阳性率的补数。`特异度 = 1 - 假阳性率`
## 敏感度与特异度的权衡
如果您正在设计一个垃圾邮件过滤器,您会希望敏感度和特异度是高还是低?与此相关的权衡是什么?
显然,我们希望它们都很高。但可以想象,当一个指标上升时,另一个往往会下降。例如,为了提高敏感度(减少漏掉的垃圾邮件),我们可能需要降低阈值,这可能导致更多的正常邮件被误判为垃圾邮件(降低特异度)。反之亦然。
因此,接下来我们将学习如何在考虑这些权衡的情况下评估模型的整体性能。
## 课程总结

本节课中,我们一起学习了分类决策中的四种可能结果:真阳性、真阴性、假阳性和假阴性。我们定义了假阳性率(第一类错误)和假阴性率(第二类错误),并引入了敏感度与特异度这两个关键性能指标。最后,我们讨论了在模型设计中敏感度与特异度之间存在的固有权衡关系,为后续学习模型性能评估方法奠定了基础。
# 050:过拟合与数据分配


在本节课中,我们将要学习模型评估中的一个核心问题:过拟合。我们将探讨什么是过拟合,为什么它是个问题,以及如何通过合理分配数据来避免它。我们将使用一个来自美国林务局的真实数据集进行演示。
## 数据准备与背景
我们使用 `forested` 包中的数据集,并照常加载 `tidyverse` 和 `tidymodels` 包。
美国林务局维护着机器学习模型,用于预测一块土地是否为林地。这不仅是学术研究,其信息对于研究、立法和土地管理决策至关重要。地块数据通常每10年重新测量一次,`forested` 数据集包含了每个地块最近一次的测量结果。
该数据集包含超过7000个观测值和19个变量。我们的结果变量是 `forested`(是或否),其余18个变量可作为预测变量,包括天气数据、地形数据以及其他政府机构的分类数据。
以下是数据概览和所有变量的列表。
结果变量是 `forested`,其水平为“是”或“否”。预测变量是18个易于获取的遥感变量,包括基于天气和地形的数值型变量,以及基于其他政府组织分类的分类变量。
你可以在包的文档中找到更多关于此数据集的信息。
在开始建模之前,我们需要暂停思考一个每个数据科学家都应问的重要问题:我们是否应该在模型中使用某个预测变量?以下是三个基本问题:
1. 使用这个变量是否符合伦理(有时甚至合法)?
2. 在预测时,这个变量是否可用?
3. 这个变量是否有助于模型的可解释性?
## 数据分割与过拟合问题
现在我们已经熟悉了数据,接下来进行数据分割和分配。
必须指出,我们在之前的视频中一直在“作弊”:我们使用全部数据来构建模型,然后在完全相同的数据上评估这些模型。在预测建模的背景下,这就像用答案钥匙学习,然后参加同一场考试并声称自己是天才。
为什么这是个问题?我们真正感兴趣的并不是对已观测数据过拟合的模型。我们想要的是一个对新数据表现同样出色的模型,以便我们能在观察到结果之前进行预测。
解决方案被称为**数据分割**或明智地分配数据。其工作原理如下:
我们将数据分成两部分:
* **训练集**:用于构建模型、估计参数和做出所有建模决策。
* **测试集**:这部分数据被“锁起来”,直到最终评估时才使用。
可以将测试集视为期末考试。在学习期间你不会偷看它,这样才能让期末考试真实衡量你的学习成果。
那么,我们应该将多少数据用于训练和测试呢?这是一个经典的权衡。用于训练的数据越多,我们得到的参数估计就越好。但用于训练的数据过多,我们就无法很好地评估预测性能;用于测试的数据过多,我们又无法很好地估计模型参数。
75/25的分割比例是一个良好的起点,但数据科学中的许多事情一样,最佳选择取决于具体情况。
`initial_split` 函数的默认分割比例就是75/25,因此我们可以用这一个函数完成数据分割。
关于可重复性的一个快速说明:你注意到我们在分割数据之前使用了 `set.seed`。这不仅是良好实践,更是可重复科学的关键。计算机生成的是伪随机数,它们看起来随机,但实际上是确定性的。只要知道种子,任何人都可以运行我们的代码并获得完全相同的结果(即相同的数据分割)。你可以选择任意喜欢的数字作为种子,只要你不尝试多个种子并挑选能给你最佳结果的那个(那又是作弊了)。
接下来,我们使用 `training` 和 `testing` 函数将分割后的数据存储为两个独立的数据框。
我们的训练数据有5030个观测值,包含所有原始变量。测试数据有1777个观测值。
## 数据探索
让我们进行一些探索。一些好的初始问题可能包括:结果变量 `Forested` 的分布如何?数值型变量(如年降水量)的分布如何?林地分布在不同分类和数值变量上有何差异?
我们应该使用哪个数据集进行探索?整个 `forested` 数据集、训练数据 `forested_train` 还是测试数据 `forested_test`?如果你说使用训练数据,那就对了——我们不能偷看测试数据。
一个简单的计数和比例表告诉我们,在训练数据中,54.7%的地块是林地,其余不是。我们预计测试数据中也有类似的分布,但我们不会去看。
观察年降水量,我们看到一个右偏分布,年均降水量非常高的年份较少。
现在,让我们看看这两个变量之间可能存在的关系。林地往往有更高的降水量,这很合理,因为树木需要水。这个填充直方图使关系更加清晰:随着降水量增加,林地的比例急剧上升,直到最后阶段关系似乎逆转,但这很可能是由于高年降水量的观测值非常少。
这是另一个例子,关于 `tree` 或 `no tree` 变量(来自卫星图像分类)。正如预期的那样,被分类为有树的区域在我们的地面实况数据中更可能是林地。但请注意,这并不完美,这就是为什么我们需要多个预测变量。
最后,我们可以查看地理模式。你实际上可以在这里看到华盛顿州的轮廓。绿点代表林地,黄色或金色代表非林地。林地更可能位于该州的西北部而非东南部,这在地理上是合理的。
## 模型评估术语与ROC曲线
让我们快速回顾一下术语,然后介绍一些新的。
* **假阴性率**:实际为阳性但被分类为阴性的比例。
* **假阳性率**:实际为阴性但被分类为阳性的比例。
* **灵敏度**:实际为阳性且被正确分类为阳性的比例,也称为真阳性率。另一个术语是**召回率**。灵敏度等于1减去假阴性率。当假阴性比假阳性代价更高时,这很有用。
* **特异性**:实际为阴性且被正确分类为阴性的比例,也称为真阴性率。特异性等于1减去假阳性率。
这里有一个新的概念:一种可视化总结灵敏度和特异性的新方法。这就是**受试者工作特征曲线**,通常称为 **ROC曲线**。ROC曲线绘制了在不同阈值下,灵敏度与(1 - 特异性)的关系。
完美模型会击中左上角,那里所有阳性都被分类为阳性,所有阴性都被分类为阴性。对角线代表随机猜测,因此我们希望尽可能远离这条线。
## 后续步骤
那么接下来做什么?我们将在训练数据上拟合模型,在测试数据上进行预测,并在测试数据上评估预测结果。
* 对于线性模型,评估指标包括 R 平方、调整后 R 平方、RMSE(均方根误差)等。
* 对于逻辑模型,评估指标包括假阴性和假阳性率、AUC(即ROC曲线下面积)等。
然后,我们将基于模型的预测性能、在不同训练/测试分割下的有效性(即交叉验证)以及可解释性来做出决策。在本课程中,我们只学习其中的一部分,但你可以在其他回归和机器学习课程中进一步探索这些概念。
## 总结

本节课中,我们一起学习了过拟合的概念及其危害。我们了解到,使用全部数据进行建模和评估是一种“作弊”行为,无法真实反映模型对新数据的预测能力。为了解决这个问题,我们引入了数据分割的方法,将数据分为训练集和测试集。我们还回顾并引入了模型评估的关键术语,如灵敏度、特异性,并介绍了ROC曲线这一强大的可视化工具。最后,我们明确了后续的建模工作流程:在训练集上拟合模型,在测试集上进行预测和评估,并基于评估结果做出决策。
# 051:森林分类 🌲


在本节课中,我们将通过一个完整的代码实践,学习如何使用R语言进行逻辑回归分类。我们将使用`forested`数据集,预测一个地块是否为森林。整个过程包括:数据分割、探索性数据分析、模型拟合、预测以及模型性能评估。
我们将使用`tidyverse`和`tidymodels`包来完成数据探索和建模工作。
---
## 数据准备与分割
首先,我们需要将数据分割为训练集和测试集。这确保了我们的模型可以在未见过的数据上进行评估,从而更可靠地衡量其性能。
为了确保结果的可重复性,我们首先设置一个随机种子。然后使用`initial_split`函数来分割数据。
```r
# 设置随机种子以确保结果可重复
set.seed(123)
# 使用 initial_split 函数分割数据
forested_split <- initial_split(forested_data)
# 查看分割结果
forested_split
```
运行上述代码后,我们可以看到大约75%的数据被分配到了训练集,25%分配到了测试集。这是`initial_split`函数的默认设置。
**如何调整分割比例?**
如果我们希望将80%的数据用于训练,20%用于测试,可以修改`initial_split`函数的`prop`参数。
```r
# 将80%的数据分配给训练集
forested_split_80 <- initial_split(forested_data, prop = 0.8)
```
接下来,我们根据分割对象创建具体的训练和测试数据集。
```r
# 创建训练集和测试集
forested_train <- training(forested_split)
forested_test <- testing(forested_split)
```
现在,我们已经准备好了用于建模和评估的数据。
---
## 探索性数据分析
在建模之前,理解数据中变量之间的关系非常重要。本节中,我们将创建一个可视化图表,探索结果变量(是否为森林)与一个数值型预测变量、一个分类型预测变量之间的关系。
我们将使用训练集`forested_train`进行探索,以避免“窥探”测试集。
首先,我们查看数据字典,了解可用的变量。例如,`temp_annual_mean`(年平均温度)是一个有趣的数值型变量。
以下是创建一个探索性可视化的步骤:
1. **选择一个数值型预测变量**:我们选择`temp_annual_mean`。
2. **选择一个分类型预测变量**:我们选择`land_type`(土地覆盖类型)。
3. **创建可视化**:我们将绘制一个按`land_type`分面的直方图,并用`forested`(是否为森林)变量填充颜色,以观察不同土地类型和森林状态下的温度分布。
```r
# 创建探索性可视化图表
exploratory_plot <- ggplot(forested_train, aes(x = temp_annual_mean, fill = forested)) +
geom_histogram(alpha = 0.7, position = "identity", bins = 30) +
scale_fill_manual(values = c("yes" = "forestgreen", "no" = "gold")) +
facet_wrap(~ land_type, ncol = 1) +
labs(title = "年平均温度分布(按土地类型和森林状态)",
x = "年平均温度 (°C)",
y = "计数") +
theme_minimal()
# 显示图表
print(exploratory_plot)
```
**图表解读**:
* 在土地类型为“树木”的区域,森林地块(绿色)的比例明显更高。
* 在土地类型为“非树木植被”的区域,非森林地块(金色)的比例更高。
* 在“贫瘠”土地类型中,数据点较少,但非森林地块的比例仍然高于森林地块。
* 总体来看,非森林地块的年平均温度似乎略高于森林地块,这与生态学常识相符。
这个可视化帮助我们直观理解了预测变量与结果之间可能存在的关系。


---
## 模型一:自定义预测变量
上一节我们探索了数据,本节中我们开始构建第一个逻辑回归模型。我们将手动选择几个预测变量来拟合模型。
我们从数据字典中选择了以下变量:`elevation`(海拔)、`tree_no_tree`(是否有树木)、`latitude`(纬度)、`longitude`(经度)和`temp_annual_mean`(年平均温度)。
```r
# 使用自定义的预测变量拟合逻辑回归模型
forested_custom_fit <- logistic_reg() %>%
set_engine("glm") %>%
fit(forested ~ elevation + tree_no_tree + latitude + longitude + temp_annual_mean,
data = forested_train)
# 查看模型摘要
tidy(forested_custom_fit)
```
模型拟合完成后,我们使用测试集进行预测,并评估模型性能。
```r
# 使用模型对测试集进行预测
forested_custom_aug <- augment(forested_custom_fit, new_data = forested_test)
# 查看预测结果的前几行
head(forested_custom_aug)
```
`augment`函数在测试数据框中添加了新的列,包括预测类别(`.pred_class`)和属于“是/否”类别的概率(`.pred_yes`, `.pred_no`)。
接下来,我们计算模型的**假阳性率**和**假阴性率**。首先,我们需要统计预测结果。
```r
# 计算混淆矩阵并得出错误率
error_rates_custom <- forested_custom_aug %>%
count(forested, .pred_class) %>%
group_by(forested) %>%
mutate(rate = n / sum(n)) %>%
ungroup() %>%
mutate(decision = case_when(
forested == "yes" & .pred_class == "yes" ~ "true_positive",
forested == "yes" & .pred_class == "no" ~ "false_negative",
forested == "no" & .pred_class == "yes" ~ "false_positive",
forested == "no" & .pred_class == "no" ~ "true_negative"
))
# 查看假阴性率和假阳性率
error_rates_custom %>%
filter(decision %in% c("false_negative", "false_positive")) %>%
select(decision, rate)
```
我们也可以使用`conf_mat`函数快速得到混淆矩阵。
```r
# 使用 conf_mat 函数计算混淆矩阵
confusion_matrix_custom <- conf_mat(forested_custom_aug,
truth = forested,
estimate = .pred_class)
print(confusion_matrix_custom)
```
为了更全面地评估模型,我们计算**敏感度**、**特异度**并绘制**ROC曲线**。
```r
# 计算ROC曲线数据
forested_custom_roc <- roc_curve(forested_custom_aug,
truth = forested,
.pred_yes)
# 绘制ROC曲线
roc_plot_custom <- ggplot(forested_custom_roc, aes(x = 1 - specificity, y = sensitivity)) +
geom_path(linewidth = 1) +
geom_abline(linetype = "dotted", color = "grey50") +
coord_equal() +
labs(title = "自定义模型ROC曲线",
x = "1 - 特异度 (假阳性率)",
y = "敏感度 (真阳性率)") +
theme_minimal()
print(roc_plot_custom)
```
ROC曲线位于对角线上方,说明模型优于随机猜测。曲线下面积(AUC)越大,模型性能越好。
---
## 模型二:使用全部预测变量
现在,我们构建第二个模型,这次使用数据集中的所有可用预测变量。这有助于我们比较“简单”模型和“复杂”模型的性能。
```r
# 使用所有预测变量拟合逻辑回归模型
forested_full_fit <- logistic_reg() %>%
set_engine("glm") %>%
fit(forested ~ ., # 公式中的‘.’代表除结果变量外的所有变量
data = forested_train)
# 查看模型摘要(可能很长)
tidy(forested_full_fit)
```
同样地,我们对这个完整模型进行预测和评估。
```r
# 使用完整模型对测试集进行预测
forested_full_aug <- augment(forested_full_fit, new_data = forested_test)
# 计算完整模型的错误率
error_rates_full <- forested_full_aug %>%
count(forested, .pred_class) %>%
group_by(forested) %>%
mutate(rate = n / sum(n)) %>%
ungroup() %>%
mutate(decision = case_when(
forested == "yes" & .pred_class == "yes" ~ "true_positive",
forested == "yes" & .pred_class == "no" ~ "false_negative",
forested == "no" & .pred_class == "yes" ~ "false_positive",
forested == "no" & .pred_class == "no" ~ "true_negative"
))
# 计算完整模型的ROC曲线
forested_full_roc <- roc_curve(forested_full_aug,
truth = forested,
.pred_yes)
```


---
## 模型比较与选择
我们已经构建了两个模型并计算了它们的评估指标。现在,我们将它们放在一起比较,以决定哪个模型更优。
首先,比较它们的假阳性率和假阴性率。
```r
# 并排比较错误率
custom_errors <- error_rates_custom %>%
filter(decision %in% c("false_negative", "false_positive")) %>%
select(model = "Custom", decision, rate)
full_errors <- error_rates_full %>%
filter(decision %in% c("false_negative", "false_positive")) %>%
select(model = "Full", decision, rate)
bind_rows(custom_errors, full_errors)
```
**结果分析**:
* **自定义模型**:假阴性率约为10.6%,假阳性率约为13.7%。
* **完整模型**:假阴性率和假阳性率均低于自定义模型。
仅从错误率来看,完整模型表现更好。
接下来,我们将两个模型的ROC曲线绘制在同一张图上,进行直观比较。
```r
# 为ROC曲线数据添加模型标识
forested_custom_roc <- forested_custom_roc %>% mutate(model = "Custom")
forested_full_roc <- forested_full_roc %>% mutate(model = "Full")
# 合并ROC曲线数据并绘图
combined_roc <- bind_rows(forested_custom_roc, forested_full_roc)


combined_roc_plot <- ggplot(combined_roc, aes(x = 1 - specificity,
y = sensitivity,
color = model)) +
geom_path(linewidth = 1) +
geom_abline(linetype = "dotted", color = "grey50") +
coord_equal() +
scale_color_brewer(palette = "Set1") +
labs(title = "模型ROC曲线比较",
x = "1 - 特异度 (假阳性率)",
y = "敏感度 (真阳性率)",
color = "模型类型") +
theme_minimal()
print(combined_roc_plot)
```
在ROC图中,曲线越靠近左上角(即敏感度高且假阳性率低),模型性能越好。从图中可以清晰看出,**完整模型(Full)的ROC曲线更接近左上角**,其曲线下面积(AUC)也更大。
**模型选择建议**:
基于更低的错误率(假阳性率、假阴性率)和更高的ROC曲线下面积,我们倾向于选择**完整模型**。它在本测试集上展现了更强的预测能力。
然而,在实际应用中,我们还需要考虑**模型简约性**原则。如果自定义模型是由领域专家基于专业知识构建的,且与完整模型性能差异不大,那么更简单、更易解释的自定义模型可能是更好的选择。本例中,我们的自定义变量是随机选取的,因此完整模型的显著优势是合理的。
---

## 总结

本节课中,我们一起完成了一个完整的逻辑回归分类项目。我们学习了:
1. **数据分割**:使用`initial_split`将数据划分为训练集和测试集,并设置随机种子保证可重复性。
2. **探索性数据分析**:通过可视化初步理解预测变量与结果变量之间的关系。
3. **模型拟合**:使用`logistic_reg()`和`fit()`函数拟合逻辑回归模型。
4. **预测**:使用`augment()`函数对测试集进行预测并获得预测类别和概率。
5. **性能评估**:
* 计算**混淆矩阵**、**假阳性率**和**假阴性率**。
* 计算**敏感度**和**特异度**。
* 绘制**ROC曲线**并计算曲线下面积(AUC)。
6. **模型比较**:通过并排比较错误率和叠加ROC曲线,选择性能更优的模型。

通过这个实践,你将逻辑回归的理论知识应用到了真实的数据分析流程中,掌握了从数据准备到模型选择的全套技能。
# 052:量化不确定性



在本节课中,我们将学习统计学和数据科学中一个至关重要的概念:如何从点估计转向理解围绕这些估计的可能性范围,即量化不确定性。
## 数据准备与探索
首先,我们需要加载必要的R包并查看数据。我们将使用`tidyverse`进行数据处理和可视化,`scales`优化坐标轴标签,`tidymodels`进行建模,以及`openintro`提供数据集。
```r
library(tidyverse)
library(scales)
library(tidymodels)
library(openintro)
```
我们分析的数据来自伊利诺伊州埃尔姆赫斯特学院大一新生的一个随机样本,包含50名学生的家庭收入和助学金信息。助学金是无需偿还的经济援助。
在散点图中,X轴代表家庭收入,Y轴代表助学金。数据显示出一种负相关关系:随着家庭收入增加,助学金倾向于减少。这从经济援助的角度来看是符合直觉的。
## 建立线性模型
为了量化这种关系,我们拟合一个线性模型。
```r
# 假设数据框名为 'elmhurst'
model <- lm(gift_aid ~ family_income, data = elmhurst)
summary(model)
```
我们的线性回归给出了一个截距(约24,300美元)和一个斜率(约-0.0431)。这个斜率意味着,家庭收入每增加1,000美元,我们预计学生获得的助学金平均会减少约43.10美元。
## 从点估计到不确定性
然而,一个关键问题随之而来:这是否意味着对于该校的每一位学生,家庭收入每增加1,000美元,我们预计其助学金都会恰好减少43.10美元?当然不是。这就是不确定性发挥作用的地方。我们计算的斜率来自一个样本。如果我们抽取不同的样本,会得到略有不同的斜率。因此,问题变成了:这个斜率的合理取值范围是多少?
这引出了**统计推断**的概念:利用样本数据对样本来源的总体做出结论的过程。统计推断提供了方法和工具,使我们能够使用单个观测样本来对其来源的总体做出有效的推断。为了使推断有效,样本应该是随机的,并且能代表我们感兴趣的总体。
## 置信区间:捕捉不确定性
到目前为止,我们做了很多估计,如均值、中位数、斜率等。我们使用样本数据计算样本统计量,然后将其用作总体参数的估计值。但从这些基于样本计算出的数字到对总体做出陈述,我们还需要考虑什么?
可以这样思考:如果你想捕鱼,你更愿意用鱼叉还是渔网?大多数人会选择渔网。同样,在估计总体参数时,你是愿意报告一个单一值,还是一个合理的取值范围?如果我们只报告一个点估计,很可能无法命中确切的总体参数。但如果我们报告一个合理的取值范围,我们就有更大的机会捕捉到真实参数。
这个合理的取值范围被称为**置信区间**。但要构建它,我们需要量化样本统计量的预期变异性。
## 量化变异性:自助法
为了构建置信区间,我们需要量化样本统计量的变异性。例如,要构建总体斜率的置信区间,我们需要围绕观测到的样本斜率提出一个合理的取值范围。这个范围取决于样本均值作为总体均值估计的精确度和准确度。量化这一点需要测量我们预期样本统计量在不同样本间会有多大变化。
我们可以通过计算方法来做到这一点,即使用一种称为**自助法**的模拟技术。或者,我们也可以通过理论(即中心极限定理)来实现。作为预览,你一直在回归输出中看到的标准误值就是基于该理论计算的。
那么,什么是自助法?这个术语来源于“靠自己的鞋带把自己拉起来”这个短语,意指在没有外部帮助的情况下完成不可能的任务。我们的“不可能任务”是:仅使用给定样本的数据来估计总体参数。
## 自助法步骤详解
以下是自助法的工作原理,分为四个步骤:
1. **抽取自助样本**:从原始样本中有放回地随机抽取一个样本,其大小与原始样本相同。
2. **计算自助统计量**:在自助样本上计算统计量,如均值、中位数、比例、斜率等。
3. **重复多次**:重复步骤1和2多次,以创建**自助分布**,即自助统计量的分布。
4. **计算置信区间边界**:将置信区间的边界计算为自助分布的中间部分,具体取决于你想要的置信水平。
让我们看看实际操作。第一个自助样本及其拟合线和斜率与原始样本相似,但不完全相同。第二个、第三个、第四个样本的斜率值都不同。当我们叠加这些样本时,可以看到拟合线是如何变化的。这种变化正是我们想要捕捉和量化的。
当然,我们不会只停留在四个样本。我们会生成成百上千个自助样本。当我们为100个自助样本拟合直线并收集所有斜率绘制成直方图时,就得到了斜率的自助分布。这向我们展示了如果我们能回到总体并抽取许多新样本,可能合理看到的斜率值范围。
## 构建与解释置信区间
从这个自助分布中,我们可以计算95%置信区间,只需找到分布的中间95%部分。
我们的斜率95%置信区间大约从-0.0699到-0.0217。
因此,我们对斜率的解释也包含了不确定性的度量:**我们有95%的置信度认为,家庭收入每增加1,000美元,我们预计学生获得的助学金平均会减少69.95美元到21.70美元**。
请注意,这比仅仅说“减少43.10美元”提供了更多的信息。这个置信区间让我们了解了估计的精确度,并承认了使用样本数据所固有的不确定性。
## 总结

本节课中,我们一起学习了如何量化不确定性。点估计是有用的,但它们不能说明全部情况。置信区间帮助我们量化不确定性,并提供了一个更完整的图景,说明我们可以从样本数据中对总体做出哪些合理的结论。自助法是一种强大、直观的计算方法,用于构建这些区间。
# 053:自助法 🎼


在本节课中,我们将学习如何使用自助法来量化统计估计中的不确定性。我们将通过一个具体的例子——估算苏格兰爱丁堡三居室公寓的平均租金——来演示自助法的完整流程,并解释如何构建和解读置信区间。
## 概述
在上一节中,我们讨论了不确定性量化的动机,并介绍了通过构建置信区间来量化抽样统计量变异性的思想。本节中,我们将通过一个新的例子来重温自助法。
## 数据准备与探索
我们将使用 `tidyverse` 进行数据处理和可视化,使用 `tidymodels` 进行建模任务。
我们的数据集包含从英国主要房产网站 `rightmove.co.uk` 上随机抽取的15套位于苏格兰爱丁堡的三居室公寓的租金信息。数据列由分号分隔,因此我们使用 `read_csv2` 函数加载数据。
```r
library(tidyverse)
library(tidymodels)
# 加载数据
rent_data <- read_csv2("path/to/your/data.csv")
```
查看数据,我们发现租金范围从每月825英镑到3250英镑不等,这反映了爱丁堡不同社区、房产状况和设施的差异。样本数据的直方图显示,大多数公寓价格在1500至2000英镑之间,存在少数高价的离群值。
我们的样本均值约为1895英镑。但关键问题是:如果我们从爱丁堡随机抽取另一组15套公寓,会得到完全相同的均值吗?几乎肯定不会。这个样本均值是我们对总体均值的最佳估计,但它伴随着不确定性。问题在于,不确定性有多大?这正是自助法要解决的问题。
## 自助法核心思想
自助法通过模拟抽样过程来估计统计量的变异性。我们通过从现有样本中**有放回地重复抽样**,来创建一个假设的“自助法总体”。
以下是自助法的分步流程:
1. **抽取自助样本**:从原始样本中有放回地随机抽取一个样本,其大小与原始样本相同。
2. **计算自助统计量**:计算该自助样本的统计量(如均值、中位数、比例等)。
3. **多次重复**:重复步骤1和2许多次,以创建**自助分布**(即自助统计量的分布)。
4. **构建置信区间**:使用该分布的中间一定百分比(例如95%)来创建我们的置信区间。
核心见解是,**有放回抽样**允许我们模拟从总体中抽取不同样本的过程。
## 在R中实现自助法
现在,让我们看看如何使用 `tidymodels` 框架(特别是 `infer` 包)在R中实现这一过程。我们将逐步构建自助分析。
首先,我们设置随机种子以确保结果可重现。由于自助法涉及随机抽样,设置种子意味着每次运行代码都会得到相同的随机样本。
```r
set.seed(123) # 确保结果可重现
```
接下来,我们使用 `specify` 函数来指定我们的模型。在本例中,我们只关心估计单个总体参数(租金均值),因此将响应变量设置为 `rent`。
```r
rent_spec <- rent_data %>%
specify(response = rent)
```
`generate` 函数是产生自助样本的地方。我们将创建15000个自助样本,每个样本大小与原始样本相同(15个)。每个自助样本都是通过从原始15个观测值中有放回地抽样创建的。
```r
rent_boot <- rent_spec %>%
generate(reps = 15000, type = "bootstrap")
```
为什么是15000次重复?更多的自助样本能让我们对抽样分布的估计更稳定,但超过一定点后,收益会递减。
然后,对于我们15000个自助样本中的每一个,我们使用 `calculate` 函数计算均值。
```r
boot_dist <- rent_boot %>%
calculate(stat = "mean")
```
现在,我们有了15000个不同的样本均值,每个都基于一个不同的自助样本。这15000个均值的集合近似于如果我们真的能从总体中抽取15000个不同样本时会看到的情况。
## 可视化与解读结果
我们可以使用 `visualize` 函数创建自助分布的直方图。
```r
boot_dist %>%
visualize()
```
这个直方图展示了样本均值的自助分布。请注意,该分布大致以我们原始样本均值1895为中心,但它显示了我们可能预期的样本间的变异性。这个分布的范围告诉了我们估计的精确度:**分布越窄,我们的样本均值就越精确**。
一个95%的置信区间由我们自助分布的中间95%界定。我们可以使用 `get_ci` 函数来计算。
```r
ci_95 <- boot_dist %>%
get_ci(level = 0.95)
ci_95
```
我们可以使用 `shade_ci` 函数将刚刚计算的置信区间边界覆盖在自助分布图上并着色。
```r
boot_dist %>%
visualize() +
shade_ci(ci_95)
```
现在,让我们来讨论这些边界(例如1601到2216)代表什么含义。以下是几个听起来合理的解释,我们来判断它们是否正确:
* **错误**:95%的时间里,样本中三居室公寓的平均租金在1601和2216之间。(这听起来像是这个单一样本的均值会随时间变化,但事实并非如此。我们知道样本均值是多少,我们已经计算过了。)
* **错误**:爱丁堡95%的三居室公寓的租金在1601和2216之间。(这是关于单个公寓的,而不是均值。我们的置信区间是关于平均租金的,而不是单个租金的范围。)
* **正确**:我们有95%的把握认为,所有三居室公寓的平均租金在1601和2216之间。(这是正确的。我们有95%的把握认为真实的总体均值——爱丁堡所有三居室公寓的平均租金——落在这个区间内。)
* **错误**:我们有95%的把握认为,这个样本中三居室公寓的平均租金在1601和2216之间。(同样,我们的样本均值是固定的。我们确切地知道它是什么,我们不需要围绕它的边界。)
## 准确度、精确度与置信水平
我们说“有95%的把握”在统计学中有非常具体的含义。**如果我们多次重复整个研究(每次收集新的15套公寓样本并构建95%的置信区间),那么大约95%的这些区间会包含真实的总体均值。**
这并不意味着我们刚刚计算出的这个特定区间有95%的概率包含真实均值。而是意味着,从长远来看,我们的方法产生的区间中有95%会包含真实的总体参数。
常用的置信区间有90%、95%和99%。更高的置信水平需要更宽的区间,以确保有更高的把握捕获真实参数。**如果我们想非常确定能捕获参数,就需要一个更宽的区间(一张更大的网)。但更宽的区间精确度较低**,它们提供的关于参数可能位置的信息不那么具体。
那么,如何同时获得高置信度和高精确度呢?答案是:**增加样本量**。更大的样本会导致样本统计量的变异性更小,这意味着在相同置信水平下,置信区间会更窄。
## 计算不同置信水平的区间
在实际操作中,我们如何改变计算置信区间边界时的置信水平呢?只需更改 `get_ci` 函数中的 `level` 参数即可。
```r
# 计算90%置信区间
ci_90 <- boot_dist %>%
get_ci(level = 0.90)
# 计算99%置信区间
ci_99 <- boot_dist %>%
get_ci(level = 0.99)
```
## 总结
本节课中我们一起学习了自助法的核心应用。我们回顾了关键概念:
* **样本统计量不等于总体参数**,但如果样本是好的,它可以是一个良好的估计。
* 我们通过**置信区间**来报告估计值,该区间的宽度取决于从总体中抽取的不同样本所产生的统计量的变异性。
* 由于我们无法持续从总体中抽样,因此我们从已有的一个样本进行**自助法抽样**,以估计抽样变异性。
* 这种方法适用于任何统计量。对于均值,我们在 `calculate` 函数中设置 `stat = "mean"`;对于中位数,则设置 `stat = "median"`。自助法本身也适用于许多其他统计量。


通过本教程,你应该能够理解自助法的原理,并在R中使用 `tidymodels` 的 `infer` 包来为各种统计量构建置信区间,从而量化你的估计中的不确定性。
# 054:杜克森林房屋自助法 🏠


在本节课中,我们将学习如何使用自助法来构建置信区间。我们将使用杜克森林社区的房屋销售数据,通过线性回归模型预测房屋价格与面积的关系,并利用自助法来估计模型斜率(即每平方英尺的增量价格)的不确定性。
我们将使用 `tidyverse`、`tidymodels` 包进行数据探索和建模,并使用 `openintro` 包来获取数据。
## 加载数据与初步探索
首先,让我们加载必要的R包并查看数据。
```r
library(tidyverse)
library(tidymodels)
library(openintro)
```
我们使用的数据来自北卡罗来纳州达勒姆市杜克森林社区的房屋销售记录,时间大约在2020年11月。这些数据最初从Zillow抓取,并包含在 `openintro` 包的 `duke_forest` 数据集中。
```r
data("duke_forest")
glimpse(duke_forest)
```
数据包含98行(即98套房屋)和多个变量,如地址、价格、面积、卧室和浴室数量等。
## 建立线性回归模型
我们感兴趣的模型是使用房屋面积来预测其价格。这是一个典型的线性回归问题。
* **结果变量**:`price`(价格,数值型)
* **预测变量**:`area`(面积,数值型)
让我们拟合一个线性回归模型。
```r
df_fit <- linear_reg() %>%
fit(price ~ area, data = duke_forest)
tidy(df_fit)
```
模型输出显示:
* **截距**:约为116,652美元。这表示当房屋面积为0时模型的预测价格,在实际中无意义,但提供了基准线。
* **斜率**:约为159美元。这表示**每增加一平方英尺面积,房屋价格预计平均上涨159美元**。
然而,这个斜率估计仅来自一个样本。为了更可靠地报告,我们需要构建一个置信区间。
## 使用自助法构建置信区间
上一节我们得到了一个点估计。本节中,我们将通过自助法来量化这个估计的不确定性。自助法的核心思想是从原始样本中**有放回地重复抽样**,生成许多“新样本”(自助样本),并在每个样本上重新拟合模型,从而得到估计量的分布。
### 第一步:计算观测拟合值
首先,我们将观测到的模型拟合结果存储起来,作为后续比较的基准。
```r
observed_fit <- linear_reg() %>%
fit(price ~ area, data = duke_forest)
```
### 第二步:生成自助样本并拟合模型
接下来,我们从原始数据中生成自助样本。为了确保结果可重复,我们首先设置随机种子。
```r
set.seed(1234) # 设置随机种子以保证结果可重复
```
现在,让我们生成自助样本。我们先从较小的数量(例如10个)开始以便理解过程,然后再增加到更合理的数量(如100个)。
以下是生成10个自助样本并拟合模型的代码:
```r
n <- 10
bootstrap_fits <- duke_forest %>%
specify(response = price, explanatory = area) %>%
generate(reps = n, type = "bootstrap") %>%
fit()
```
代码解析:
1. `specify(...)`: 指定模型公式。
2. `generate(reps = n, type = "bootstrap")`: 生成 `n` 个自助样本。`bootstrap` 类型意味着有放回抽样,且每个新样本大小与原始样本相同(98个观测值)。
3. `fit()`: 在每个自助样本上拟合线性回归模型。
查看结果 `bootstrap_fits`,我们会发现每个自助样本都产生了截距和斜率两个估计值。我们关心的是斜率(`term == "area"`)。
### 第三步:可视化自助分布
让我们将自助样本数量增加到100,并存储结果,然后可视化斜率的自助分布。
```r
set.seed(1234)
n <- 100
bootstrap_fits <- duke_forest %>%
specify(response = price, explanatory = area) %>%
generate(reps = n, type = "bootstrap") %>%
fit()
# 提取斜率估计并绘制直方图
bootstrap_fits %>%
filter(term == "area") %>%
ggplot(aes(x = estimate)) +
geom_histogram(binwidth = 10) +
labs(title = "Bootstrap Distribution of Slope (Price per Sq Ft)",
x = "Slope Estimate ($ per sq ft)",
y = "Count")
```
这张直方图展示了基于我们原始样本的100个自助样本所产生的斜率估计的分布情况,直观地反映了估计的变异性。
### 第四步:计算百分位置信区间
现在,我们利用这个自助分布来计算95%置信区间。我们将使用 `get_confidence_interval()` 函数,并指定使用百分位法。
```r
ci_95 <- get_confidence_interval(
bootstrap_fits,
point_estimate = observed_fit,
level = 0.95,
type = "percentile"
)
ci_95
```
结果解释:我们有95%的置信度认为,对于杜克森林的房屋,**每增加一平方英尺面积,其价格预计会上涨92.4美元到206美元**。
### 第五步:探索不同置信水平
我们可以轻松修改代码来计算不同置信水平(如90%和99%)的区间。
以下是计算90%置信区间的代码:
```r
ci_90 <- get_confidence_interval(
bootstrap_fits,
point_estimate = observed_fit,
level = 0.90,
type = "percentile"
)
ci_90
```
以下是计算99%置信区间的代码:
```r
ci_99 <- get_confidence_interval(
bootstrap_fits,
point_estimate = observed_fit,
level = 0.99,
type = "percentile"
)
ci_99
```
比较这三个区间:
* **90% CI**:最窄(最精确),但置信水平最低。
* **95% CI**:常用的平衡选择。
* **99% CI**:最宽(最不精确),但置信水平最高,即最有可能包含真实的总体参数。
选择哪个置信水平取决于研究目标:
* 若想**高度确信**捕获真实参数,应使用更宽的区间(如99% CI),但这会降低信息的精确性。
* 若想获得**更精确**的估计,应使用更窄的区间(如90% CI),但这意味着犯错的风险略有增加。
## 总结
本节课中我们一起学习了自助法的完整应用流程:
1. 我们基于杜克森林房屋数据,建立了以面积为预测变量、价格为响应变量的线性回归模型。
2. 我们理解了单一斜率点估计的局限性,并引入了自助法来评估估计的不确定性。
3. 我们通过**有放回地重复抽样**从原始数据生成多个自助样本,并在每个样本上重新拟合模型,从而构建了斜率估计的**自助分布**。
4. 我们利用自助分布计算了不同置信水平(90%, 95%, 99%)的**百分位置信区间**。
5. 最后,我们讨论了在**置信度(准确性)** 和**区间宽度(精确性)** 之间进行权衡的概念,以及如何根据研究目的选择合适的置信水平。

自助法是一种强大且直观的统计工具,它不依赖于复杂的理论假设,仅通过重抽样就能对估计量的变异性进行稳健的推断。
# 055:假设检验


在本节课中,我们将学习一种重要的统计推断方法:假设检验。我们将了解如何利用数据来评估关于总体参数的相互对立的两种主张,并学习如何通过模拟来计算P值,从而做出统计决策。
上一节我们介绍了通过置信区间进行参数估计,本节中我们来看看如何通过假设检验来进行统计决策。
## 假设检验概述
假设检验是一种利用数据来评估相互对立的主张的统计技术。这与参数估计不同。我们不再问“参数的值是多少”,而是问“我们的数据是否支持关于参数的某个特定主张”。
我们总是有两个相互对立的假设:
* **零假设**:通常记作 **H₀**。它代表现状或“无事发生”的立场。这是关于总体的一个假设,我们将对其进行检验。
* **备择假设**:通常记作 **Hₐ**。它代表我们的研究问题或“有事发生”的立场。这是我们试图寻找证据支持的观点。
一个重要的注意事项是:假设总是关于**总体参数**的,而不是我们可以直接计算的**样本统计量**。
## 一个具体例子
假设我们想知道在杜克森林社区,房屋面积是否与价格相关。
* **零假设** 将陈述“无事发生”:房屋面积与价格之间关系的斜率为0。换句话说,**β₁ = 0**。这意味着面积无助于预测价格。
* **备择假设** 则陈述“有事发生”:斜率不等于0。换句话说,**β₁ ≠ 0**。这意味着面积有助于预测价格。
这被称为**双侧检验**,因为我们检验的是斜率是否在任一方向上偏离0。我们可能对斜率应为正或负有自己的看法,但我们不将这些看法带入假设检验框架。我们倾向于将备择假设设定为关系可能朝任一方向发展。
## 假设检验的核心思想
假设检验的关键思维方式是:**假设你生活在一个零假设为真的世界里**。即假设 **β₁ = 0**。然后我们问自己:在这个世界里,观察到我们现有的样本统计量(或更极端的情况)的可能性有多大?
换句话说,如果真实的总体斜率确实是0,那么观察到样本斜率是-159或更低,或是+159或更高的概率是多少?(注:159来自之前视频中计算的样本斜率值。)
这听起来可能是一种反直觉的决策方式。但它也是一种常用的方法,类似于美国的法庭审判。零假设是“被告无罪”,备择假设是“被告有罪”。然后我们出示证据(收集数据),并判断证据:如果被告真的无罪,这些证据是否可能偶然出现?如果是,我们“无法拒绝”零假设,得出“无罪”裁决;如果不是,我们“拒绝”零假设,支持备择假设,得出“有罪”裁决。就像在法庭上一样,我们无法证明无罪,我们只能找到有罪的充分证据,或者找不到。
## 假设检验的步骤
以下是进行假设检验的一般步骤:
1. 从代表现状的零假设 **H₀** 开始。
2. 设定代表研究问题的备择假设 **Hₐ**。
3. 在零假设为真的假设下进行检验,并计算**P值**——在零假设为真的条件下,观察到当前样本结果或更极端结果的概率。
4. 如果检验结果表明数据没有为备择假设提供有说服力的证据,我们坚持零假设。如果提供了,则我们拒绝零假设,支持备择假设。
## 模拟零分布与计算P值
我们已经计算过观察到的样本斜率是159。接下来,我们通过模拟来构建零分布。
我们首先设定随机种子以确保结果可重现。然后从我们的数据框 `duke_forest` 开始,指定模型 `price ~ area`。我们的零假设声明两者独立(即斜率β₁为0)。接着,我们生成随机样本(这里用1000次),这次生成的方法是**置换**。置换会打乱y值(价格),同时保持x值(面积)固定,从而打破它们之间的任何关系,模拟如果零假设为真时我们会看到的情况。最后,我们对这1000个随机生成的样本分别拟合模型。
以下是实现这一过程的R代码:
```r
# 加载必要的包
library(tidyverse)
library(tidymodels)
library(openintro) # 包含数据集
# 查看数据
data(duke_forest)
# 设定随机种子
set.seed(12345)
# 模拟零分布(置换法)
null_dist <- duke_forest %>%
specify(response = price, explanatory = area) %>%
hypothesize(null = "independence") %>% # 设定零假设为“独立”
generate(reps = 1000, type = "permute") %>% # 生成1000个置换样本
fit() # 对每个样本拟合线性模型
# 查看结果(包含截距和斜率)
null_dist
# 可视化零分布(斜率部分)
visualize(null_dist) +
shade_p_value(obs_stat = 159, direction = "both")
```
我们主要关注斜率。通过可视化,我们可以看到斜率的零分布。它以0(零假设值)为中心,呈单峰对称分布,就像自助法分布一样。图中的红线标记在159,这是我们观察到的样本斜率。我们的观察斜率似乎远在这个分布的尾部之外。
现在我们要问:如果我们真的生活在一个零假设为真的世界里(即房屋面积与价格无关,斜率β₁=0),那么随机抽取98套杜克森林房屋,观察到样本统计量为159或在任一方向上更极端的情况,可能性有多大?
换句话说,在1000次模拟中,有多少次纯粹由于偶然性,我们观察到了比实际观测值(在任一方向上)更极端的样本统计量?
答案是:**一次也没有**。
我们也可以让R显式计算这个概率,即**P值**:
```r
# 计算P值
null_dist %>%
get_p_value(obs_stat = 159, direction = "both")
```
R给出的P值约为0,但同时有一个重要警告:这个0是基于我们设定的模拟次数(1000次)的近似值。很可能,如果我们有更高的模拟次数(例如15000次),我们可能会观察到少数几个具有更极端样本统计量的样本,但我们预计这个数字会非常低。
## 做出结论
基于计算出的P值,假设检验的结论是什么?
* 如果P值很小(我们通常使用0.05作为阈值来判断是否“小”),我们**拒绝零假设**。我们的P值近似为0,确实很小。因此,我们得出结论:**有令人信服的(或统计上可辨别的)证据表明房屋面积与价格相关**。我们在样本中观察到的关系不太可能仅由偶然因素造成。
* 另一方面,如果P值很大(大于我们5%的显著性水平),我们将**无法拒绝零假设**,并说我们没有可辨别的证据来断定存在关系,观察到的关系可能只是偶然的。
这种方法的美妙之处在于,我们使用了与自助法相同的模拟技术,但这次我们是在一个特定的假设(零假设)下进行模拟。
## 总结


本节课中我们一起学习了假设检验的基本框架。我们了解了零假设与备择假设的区别,掌握了通过置换模拟构建零分布并计算P值的方法,并学会了如何根据P值做出“拒绝”或“无法拒绝”零假设的统计决策。假设检验使我们能够基于数据,对关于总体参数的特定主张进行严谨的评估。

浙公网安备 33010602011771号