章节概述

本章讨论社会科学数据进入分析前需要完成的基本工作。第一部分区分 R 对象的类型、类与结构,并用变量、观测和取值解释整洁数据。随后以 tidyverse 为主要工具,练习选择行列、重命名变量、构造变量、分组汇总、连接表格和长宽转换;每项操作都配有输入、输出与结果检查。

数据处理还包括质量诊断。缺失值、重复记录、文本格式和列名需要依据数据字典与研究设计处理,不能只以代码能否运行为标准。最后一部分使用数据概览、描述统计、分组比较和相关分析检查分布、异常记录与变量关系,并区分描述性结果、探索性线索与因果解释。

本章学习内容

学习导航

1.0 数据类型与结构

1.0 数据类型与结构

R 中的每个对象都有类型和结构。类型说明值如何存储,结构说明这些值如何组合。读懂这两层信息,才能判断一段代码为什么能够计算,或为什么会报错。

1.1 常用数据类型

R 把数值、文本和逻辑值保存在对象中。数据类型(data type)描述对象在内存中的基本存储方式;对象类(class)则告诉 R 应采用哪一组方法处理该对象。typeof() 和 class() 因此可能返回不同结果。

R 中常见底层类型、适用值与 typeof() 结果
常用类型 适合保存的值 示例 typeof()
logical 逻辑判断结果 TRUE、FALSE "logical"
integer 明确写成整数的数值 23L、-3L "integer"
double R 中常见的数值 23、3.14 "double"
character 文本、编码和标签 "上海"、"A01" "character"

普通数字 23 的对象类是 numeric,底层类型是 double;在数字后加 L 得到整数 23L。这属于存储差异,不能直接用来判断社会科学变量是离散变量还是连续变量。

将下列代码保存在项目脚本 scripts/01_objects.R 中,可从上到下逐行运行并比较输出。

score <- 23
student_count <- 23L
city <- "上海"
completed <- TRUE

class(score)
##  [1] "numeric"
typeof(score)
##  [1] "double"
class(student_count)
##  [1] "integer"
typeof(student_count)
##  [1] "integer"

常见问题:数字被读成文本

引号中的 "13" 是字符值,不能直接参与加法。Excel 或 CSV 的某一列混入单位、空格或文字时,也可能被导入为字符型。

response <- "13"
class(response)
## [1] "character"

此时运行 response + 2 会报错,因为加法要求数值输入。确认 "13" 确实表示数字后,再创建转换后的新对象。

response_num <- as.numeric(response)
response_num + 2
## [1] 15

先检查原始值,再进行转换。直接覆盖原对象可能掩盖录入错误或特殊编码。

进阶了解:转换失败、NA 与缺失值检查

as.numeric() 只能转换可以识别的数字文本。无法识别的值会产生 warning,并转换为缺失值 NA。

raw_response <- c("13", "未回答")
numeric_response <- as.numeric(raw_response)
is.na(numeric_response)

is.na() 会逐个判断元素是否缺失。此处只需认识 NA;缺失原因和处理方法将在 4.0 集中讨论。


1.2 核心数据结构

数据结构(data structure)描述多个值怎样组合成一个对象。选择结构时主要看三个问题:对象有几维、各元素能否使用不同类型,以及后续需要按位置还是按名称提取。

常见 R 数据结构的维度、类型规则与提取方式
对象 维度 类型规则 常用提取
向量(vector) 一维 元素使用同一种原子类型 [ ]
列表(list) 一维 成员可以使用不同类型和结构 [[ ]] / $
矩阵(matrix) 二维 单元格使用同一种原子类型 [行, 列]
因子(factor) 一维 分类标签由 levels 保存 [ ] / levels()
数据框(data frame) / tibble 二维 各列等长;列与列的类型可以不同 [[ ]] / $ / [行, 列]

常用创建函数包括 c()、list()、matrix()、factor()、data.frame() 和 tibble::tibble()。原子向量只能使用一种类型,例如 c(13, "未回答") 会形成字符向量。[ ] 可以按位置、名称或逻辑条件保留一个或多个元素;[[ ]] 从列表或数据框中取出一个成员;$ 也可按名称提取,例如 student_data$city,但名称必须直接写在代码中。

交互式代码观察
对象与结构检查器

切换对象,比较相同检查函数在不同结构上的返回结果。

R 代码
score_vec <- c(
  72, 88, 91
)
score_vec[2]
typeof()
double
class()
numeric
规模
长度 3;没有二维维度
提取结果
88

向量中的三个元素都按双精度数值存储。

进阶了解:数组与因子的底层表示

数组(array)把同一种原子类型排列到两个以上的维度中;矩阵可以看作二维数组。课程后续主要处理表格数据,因此只需能够识别数组。

score_array <- array(1:8, dim = c(2, 2, 2))
score_array[1, 2, 2]

region_factor <- factor(c("东部", "西部", "东部"))
levels(region_factor)
unclass(region_factor)

因子在底层使用整数编码,并用 levels 保存显示标签。分析时应根据标签和水平顺序解释因子,不应把底层整数当作可直接计算的测量值。


1.3 读懂表格数据

社会科学数据常以行和列组成表格。在 R 中,常见表格对象是 data.frame 和 tibble。tibble 继承 data.frame,主要改进打印方式和部分子集操作的行为;两者都由等长列组成。

案例数据:Gapminder

gapminder 包提供同名的 gapminder 数据对象,整理了 Gapminder 项目的历史国家数据。该表包含 1,704 条记录和 6 个变量,涉及 142 个国家、5 个大洲和 12 个年份。

时间范围
1952—2007 年,每 5 年一次
观测单位
一个国家在一个年份
指标单位
lifeExp 为出生时预期寿命(年),pop 为人口,gdpPercap 为人均 GDP
使用边界
历史教学样本,不能描述各国当前状况

其余两列是国家 country 和大洲 continent。字段名来自数据包原表,本章沿用 lifeExp 和 gdpPercap 等原名。

1.3.1 加载数据与阅读 tibble

library(gapminder) 在当前 R 会话中加载包;随后输入 gapminder,即可预览同名数据对象。代码块关闭包加载消息和 warning,使网页只保留数据输出。在 Console 中运行时,仍可能看到与本机包版本有关的提示。

library(gapminder)
gapminder
  # A tibble: 1,704 × 6
     country     continent  year lifeExp      pop gdpPercap
     <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
   1 Afghanistan Asia       1952    28.8  8425333      779.
   2 Afghanistan Asia       1957    30.3  9240934      821.
   3 Afghanistan Asia       1962    32.0 10267083      853.
   4 Afghanistan Asia       1967    34.0 11537966      836.
   5 Afghanistan Asia       1972    36.1 13079460      740.
   6 Afghanistan Asia       1977    38.4 14880372      786.
   7 Afghanistan Asia       1982    39.9 12881816      978.
   8 Afghanistan Asia       1987    40.8 13867957      852.
   9 Afghanistan Asia       1992    41.7 16317921      649.
  10 Afghanistan Asia       1997    41.8 22227415      635.
  # ℹ 1,694 more rows
读懂控制台输出
# A tibble: 1,704 × 6
对象有 1,704 行、6 列。
<fct> <int> <dbl>
列名下方显示各列的对象类或底层类型缩写。
ℹ 1,694 more rows
当前只打印前 10 行,其余记录仍保存在对象中。

<int> 与 <dbl> 描述存储方式。变量是计数、年份还是连续测量值,仍由数据含义决定。

tibble 在 R Console 中会根据可用宽度显示适量的行和列,因此打印行数可能随设置变化。Knit 写入网页后,打印结果保留生成时的排版。需要明确控制预览范围时,可使用 head() 或 print(n = ...)。

生成 HTML 表格

Knit 会在独立的 R 会话中按顺序运行代码,并把结果写入 HTML。knitr::kable() 可将数据框转为适合网页阅读的表格。

knitr::kable(
  head(gapminder, 3),
  caption = "Gapminder 数据集前 3 行",
  digits = 1
)
Gapminder 数据集前 3 行
country continent year lifeExp pop gdpPercap
Afghanistan Asia 1952 28.8 8425333 779.4
Afghanistan Asia 1957 30.3 9240934 820.9
Afghanistan Asia 1962 32.0 10267083 853.1

digits = 1 控制数值列的显示精度,不会改写 gapminder 对象中的原始值。

1.3.2 行、列与观测单位
gapminder 中的观测、变量与数值
表格要素 含义 在 gapminder 中的对应内容
数据集(dataset) 围绕同一主题组织的整张表 整个 gapminder 表
变量(variable) 描述某项属性的一列 country、year、lifeExp 等
观测(observation) 表中的一行记录 某国某年的一组指标
观测单位(unit of observation) 一行对应的对象与情境 国家 × 年份

country 和 year 共同定位一条记录。只看 country 会遗漏时间差异,只看 year 会混合多个国家。确认观测单位后,才能判断重复记录、缺失值和汇总结果是否合理。

1.3.3 初步检查数据

初次读取一张表时,可依次检查规模、列名、对象类、列结构、首尾记录和缺失情况。下面的代码都使用已经加载的 gapminder 对象,可以直接运行。

dim(gapminder)       # 行数与列数
names(gapminder)     # 列名
class(gapminder)     # 对象类
str(gapminder)       # 各列结构与部分取值
head(gapminder)      # 前 6 行
tail(gapminder)      # 后 6 行
anyNA(gapminder)     # 是否存在 NA

str() 把对象类、维度和逐列信息集中显示,适合第一次检查数据。

str(gapminder)
  tibble [1,704 × 6] (S3: tbl_df/tbl/data.frame)
   $ country  : Factor w/ 142 levels "Afghanistan",..: 1 1 1 1 1 1 1 1 1 1 ...
   $ continent: Factor w/ 5 levels "Africa","Americas",..: 3 3 3 3 3 3 3 3 3 3 ...
   $ year     : int [1:1704] 1952 1957 1962 1967 1972 1977 1982 1987 1992 1997 ...
   $ lifeExp  : num [1:1704] 28.8 30.3 32 34 36.1 ...
   $ pop      : int [1:1704] 8425333 9240934 10267083 11537966 13079460 14880372 12881816 13867957 16317921 22227415 ...
   $ gdpPercap: num [1:1704] 779 821 853 836 740 ...
tibble 控制台输出符号的读取方法
输出片段 读取方法
tibble [1,704 × 6] (S3: tbl_df/tbl/data.frame) 1,704 行、6 列;兼容 data.frame
$ country : Factor w/ 142 levels ... country 是含 142 个水平的因子列
$ year : int [1:1704] 1952 1957 ... year 以整数存储,共 1,704 个值

anyNA(gapminder) 返回 FALSE,表示这张表没有 NA。这项检查只能回答“是否出现缺失值”,不能说明样本是否完整或数据是否具有代表性。

进阶了解:S3 类标签与传统 data.frame

class(gapminder) 返回 tbl_df、tbl 和 data.frame 三个类标签。R 会按顺序寻找适用的方法,这使 tibble 能保留数据框兼容性,同时使用自己的打印和子集行为。

class(gapminder)
class(as.data.frame(gapminder))

暂时只需记住:class() 可能返回多个标签,排在前面的类通常决定更具体的处理方式。


1.4 小节自测

选择答案后点击“检查答案”,页面会显示对应解析。

01对普通数字 23 分别运行 class() 和 typeof(),会得到什么?

02下面哪项准确描述了数据框?

03gapminder 中一行数据的观测单位是什么?

课外阅读与函数文档

下面四个页面分别对应本节的类型、对象类、tibble 和案例数据。无法联网时,可在 Console 中运行 ?typeof、?class 或 vignette("tibble", package = "tibble") 查看本地帮助。

2.0 整洁数据

2.0 整洁数据

整洁数据(tidy data)是一套组织数据的约定:让变量、观测和取值在表格中各有明确的位置,便于理解、分析与复核。

调查问卷、业务记录和统计报表常按不同方式保存。面对同类分析任务,研究者因而需要反复辨认和调整数据结构。Hadley Wickham 在 2014 年发表的 Tidy Data 中系统阐述了整洁数据框架,目的是通过一致的数据结构减少重复整理,让数据处理、建模和可视化工具更容易衔接(Wickham,2014)。

2.1 概念与规则

整洁性取决于数据的含义与表格结构是否对应。理解规则之前,需要先区分三个概念:变量(variable)记录某一种属性,观测(observation)记录研究对象在指定条件下的情况,取值(value)则是某条观测在某个变量上的具体内容。

例如,研究两座城市的公共图书馆使用情况,可以把“一座城市在一个年份的记录”定义为一条观测。下表包含城市、年份、到馆人次三个变量,共有四条观测;到馆人次以万人次计,数值均为教学模拟数据。

在同一张表中辨认三个概念
两市公共图书馆年度到馆人次(模拟数据)
城市年份到馆人次(万人次)
A 市2019120
A 市2020110
B 市201995
B 市202090

每个变量各占一列

变量表示记录的某一种属性。“到馆人次”这一列只记录到馆人次,各行使用相同的计量单位;城市和年份分别放在另外两列。列中的数值可以不同,但含义应一致。

每条观测各占一行

本例的一条观测是一座城市在一个年份的记录。最后一行把“B 市”“2020 年”和“90 万人次”放在一起。B 市在 2019 年的记录属于另一条观测,因此城市名称可以在不同的行中重复。

每个单元格保存一个值

取值是某条观测在某个变量上的具体内容。右下角的“90”表示 B 市 2020 年的到馆人次。它的含义需要结合所在行、列名和计量单位一起理解;数字和城市名称都可以是取值。

这三条表内规则采用 R for Data Science 的表述。Wickham(2014)还强调,每类观测单位各成一张表。例如,图书馆基本信息可以每馆一行,城市年度统计则每个城市每年一行;两类记录分别保存,并注明城市等关联信息。

整洁数据示意图:城市、年份、到馆人次三个变量对应三列,四条城市年度记录对应四行;蓝色标示变量列,绿色标示一条观测,紫色标示数值90。下方用相同数据说明年份分列和信息合存的情况。

整洁数据中变量、观测与单元格的对应关系

2.2 常见排法

先明确变量的含义和观测单位,再判断排法。 行列数量、表格外观或单元格颜色,都不足以单独判断数据是否整洁。

年份写入列名

下面仍是两座城市的四个年度数值,每行把同一城市的两年数据并列展示。

按年份并列展示到馆人次(万人次,模拟数据)
城市 2019 2020
A 市 120 110
B 市 95 90

在“每个城市每年一条观测”的约定下,年份本身是一个变量。这里把年份的取值写进列名,一行便包含了两条年度观测。与前面的四行表相比,数值没有改变,变量和观测所处的位置改变了。

宽表一定不整洁吗?

宽表便于并排阅读,是否整洁仍取决于各列的含义。若一人只测量一次,身高和体重是两个不同的变量,适合各占一列;它们的单位分别为厘米和千克。将各年数值排在多列,与把身高、体重排在两列,需要分别判断。

多个变量合存

如果 F_23 约定表示“女性、23 岁”,同一个单元格就同时记录了性别与年龄。只记录 F_23,无法直接从某一列读出所有人的年龄;把性别与年龄作为两个变量记录,含义会更清楚。这里 F 的含义依赖编码说明,不能仅凭字母猜测。

日期时间也包含多个变量吗?

时间戳(timestamp)如 2025-04-29 19:03:52 表示一个时间点,可以作为一个变量的取值。字符串里有空格或符号,并不意味着一个单元格包含多个变量;需要依据记录的含义判断。

观测单位混排

若一行表示一名学生的成绩,就应按这一约定理解所有数据行。下表最后一行代表整个小组,与前两行的个人记录不同。

个人记录与小组汇总混排(模拟数据)
小组 学生 记录含义 成绩
G1 A01 个人成绩 82
G1 A02 个人成绩 74
G1 不对应个人 小组均值 78

这张表包含两名学生和一条小组汇总,不能按三名学生理解。个人记录与小组统计分别成表,有助于保持每张表中观测单位的一致性。若只在个人表中附加一列“所在小组均值”,一行仍对应一名学生,与混入小组汇总行的情况不同。

2.3 用途与边界

一致的数据结构使分析任务更容易对应到明确的变量和观测。在城市年度案例中,这种对应关系体现在:

便于比较
比较 2019 年与 2020 年时,年份列标明各条记录所属的时间。
便于分析
城市、年份和到馆人次各有明确位置,筛选、汇总与绘图可以围绕同一套变量展开。
便于复核
通过“B 市、2020 年”定位到同一条记录,再核对其到馆人次是否为 90 万人次。

整洁结构与数据质量需要分别检查

表格符合整洁规则,仍可能存在缺失值、重复记录或错误数值。例如,同一个城市同一年出现两条重复记录,或到馆人次被误填为负数,行列外观依然可以十分规整。整洁规则说明如何组织数据,数值是否可信还需要结合来源、计量单位与调查过程判断。

借助 AI 识读表格时,需要说明什么?

可以同时提供观测单位、变量含义与计量单位。例如:“每行是一座城市某一年的记录,到馆人次以万人次计。”这些说明可作为核对 AI 解释的依据。整洁结构不能保证 AI 正确理解变量,更不能替代对原始记录的核验;涉及个人信息时,应先按数据使用要求处理,避免上传可识别个人的记录。

2.4 小节自测

01本例中,B 市 2019 年和 2020 年的记录应如何理解?

02关于整洁数据,下列哪项理解正确?

概念来源与延伸阅读

3.0 tidyverse 操作

3.0 tidyverse 操作

3.1 tidyverse 与数据准备

tidyverse 是一组为数据分析设计的 R 包。 这些包采用相近的写法,分别承担数据读取、整理、计算和绘图等任务。对于表格数据,常见操作包括选择行列、计算新变量、分组汇总和关联多张表。

tidyverse 也指统一安装和加载这组核心包的入口包。运行 library(tidyverse) 后,常用函数便可在当前 R 会话中调用。本节重点使用 dplyr 和 tidyr:

本节使用的 tidyverse 核心包及其分工
包 在数据分析中的作用 本节中的例子
dplyr 选择行列、计算变量、分组与连接 filter()、mutate()、summarise()
tidyr 调整变量与观测在表中的排列 pivot_longer()、separate_wider_delim()
tibble 创建和显示数据表 tibble()
readr 读取文件和解析数值 parse_integer()

核心包还包括 ggplot2、purrr、stringr、forcats 和 lubridate。它们的分工见 tidyverse 官方包说明。加载包不要求立即掌握其中全部函数,先围绕具体的数据任务选择工具。

准备练习数据

下列示例使用 gapminder:每行记录一个国家或地区在某年的出生时预期寿命、人口和人均 GDP。数据覆盖 1952—2007 年,每五年一期。lifeExp 与 gdpPercap 是数据集的原始列名;练习副本将其改为 life_exp 与 gdp_per_cap,便于统一命名。

library(tidyverse)
library(gapminder)

# 保留原始列名的数据副本
test_data <- gapminder

# 本节使用的练习副本
practice_data <- test_data %>%
  rename(life_exp = lifeExp, gdp_per_cap = gdpPercap)

head(practice_data, 3)
# A tibble: 3 × 6
  country     continent  year life_exp      pop gdp_per_cap
  <fct>       <fct>     <int>    <dbl>    <int>       <dbl>
1 Afghanistan Asia       1952     28.8  8425333        779.
2 Afghanistan Asia       1957     30.3  9240934        821.
3 Afghanistan Asia       1962     32.0 10267083        853.

运行准备:如尚未安装所需包,可在 Console 中运行 install.packages(c("tidyverse", "gapminder")),安装一次即可。每次新建 R 会话仍需运行上方准备代码。直接进入某个函数页面时,也先完成这一步。

在 RStudio 脚本中,Windows 使用 Ctrl + Enter,macOS 使用 Cmd + Enter 运行选中代码。结果通常显示在控制台(Console);View(practice_data) 打开的是数据查看器(Data Viewer)。

管道与赋值

管道符 %>% 通常将左侧结果作为右侧函数的第一个参数。因此,practice_data %>% head(3) 与 head(practice_data, 3) 等价。多步操作按从上到下的顺序执行:

data_2007 <- practice_data %>%
  filter(year == 2007) %>%
  select(country, life_exp)

head(data_2007, 3)
# A tibble: 3 × 2
  country     life_exp
  <fct>          <dbl>
1 Afghanistan     43.8
2 Albania         76.4
3 Algeria         72.3

这里先筛选 2007 年的记录,再保留两列,最后通过 <- 保存为 data_2007。只运行管道并打印结果,不会改写原对象。 后面的示例也遵循这一规则。

3.2 常用处理函数

函数导航

从数据任务选择函数

下面 12 项按行列操作、计算汇总和表格整理排列。每项先给出基础用法;参数交互和折叠区用于观察差异或进一步练习。

读图约定: 示意图中的中文字段用于辨认变量含义,正文代码块是可复制、可运行的课程版本。原始列名若含空格、括号等符号,需要使用反引号包围;课程代码继续采用 snake_case。

概览:glimpse()
3.2.1 数据概览

glimpse() 将每个变量放在一行中显示,依次列出变量名、数据类型和部分取值。打印结果便于检查行列规模、数字是否被存成文本等问题,原数据的行列结构保持不变。

practice_data %>% glimpse()
Rows: 1,704
Columns: 6
$ country     <fct> "Afghanistan", "Afghanistan", "Afghanistan", "Afghanistan"…
$ continent   <fct> Asia, Asia, Asia, Asia, Asia, Asia, Asia, Asia, Asia, Asia…
$ year        <int> 1952, 1957, 1962, 1967, 1972, 1977, 1982, 1987, 1992, 1997…
$ life_exp    <dbl> 28.801, 30.332, 31.997, 34.020, 36.088, 38.438, 39.854, 40…
$ pop         <int> 8425333, 9240934, 10267083, 11537966, 13079460, 14880372, …
$ gdp_per_cap <dbl> 779.4453, 820.8530, 853.1007, 836.1971, 739.9811, 786.1134…

输出开头的 Rows 与 Columns 分别表示行数和列数。<int> 表示整数,<dbl> 表示双精度数值,<fct> 表示因子。这里只显示部分取值,不能凭预览认定整列没有缺失或录入错误。

左侧是四行三列的城市年度到馆人次表,右侧是 glimpse 输出;输出依次显示四行、三列,以及城市、年份、到馆人次的类型和部分取值

glimpse() 按变量列出名称、类型和部分取值,原表保持不变

选列:select()
3.2.2 变量选择

select() 选择数据的列。按名称列出需要的变量,结果就按给定顺序排列;在列名前加 - 可以排除该列。普通未分组数据的行数保持不变。

practice_data %>%
  select(country, year, life_exp) %>%
  head(3)
# A tibble: 3 × 3
  country      year life_exp
  <fct>       <int>    <dbl>
1 Afghanistan  1952     28.8
2 Afghanistan  1957     30.3
3 Afghanistan  1962     32.0
左侧表格包含城市、年份和到馆人次,年份列被淡化;右侧结果只保留城市与到馆人次,四条记录及其数值不变

select() 保留城市与到馆人次两列,四条观测保持不变

按列名、前缀和类型选择变量
选择方式 可运行的写法
排除人口列 practice_data %>% select(-pop)
按前缀选择列 practice_data %>% select(starts_with("life"))
选择数值列 practice_data %>% select(where(is.numeric))

数值列也可能包含年份或编号。变量的统计含义仍需单独判断,不能把数值列全部当作需要求均值的指标。

选学:必需列、可选列与正则选择

列名保存在字符向量中时,all_of() 要求每一列都存在,适合检查必需变量;any_of() 忽略不存在的列,适合选择可选变量。

required_cols <- c("country", "year", "life_exp")
practice_data %>% select(all_of(required_cols)) %>% head(3)
# A tibble: 3 × 3
  country      year life_exp
  <fct>       <int>    <dbl>
1 Afghanistan  1952     28.8
2 Afghanistan  1957     30.3
3 Afghanistan  1962     32.0
optional_cols <- c("life_exp", "pm25_mean")
practice_data %>% select(any_of(optional_cols)) %>% head(3)
# A tibble: 3 × 1
  life_exp
     <dbl>
1     28.8
2     30.3
3     32.0

matches() 按正则表达式匹配列名。例如,practice_data %>% select(matches("^life|_cap$")) 保留以 life 开头或以 _cap 结尾的列。复杂规则宜先检查匹配到的列名。

按位置写 select(1:3) 也可以选择列,但列顺序改变后,结果含义可能随之改变。长期使用的脚本优先写出列名。

列名:rename()
3.2.3 列名与列序

rename() 使用 新列名 = 原列名 的顺序。relocate() 调整列的位置,便于把标识列和相关指标放在一起。两者都不修改单元格中的数值。

renamed_data <- practice_data %>%
  rename(population = pop) %>%
  relocate(year, .after = country)

head(renamed_data, 3)
# A tibble: 3 × 6
  country      year continent life_exp population gdp_per_cap
  <fct>       <int> <fct>        <dbl>      <int>       <dbl>
1 Afghanistan  1952 Asia          28.8    8425333        779.
2 Afghanistan  1957 Asia          30.3    9240934        821.
3 Afghanistan  1962 Asia          32.0   10267083        853.

这里把 pop 改为 population,并将 year 移到 country 后面。.before 可指定放在哪列之前;省略位置参数时,relocate() 将选定列移到最前面。

左侧原表的第三列名为到馆人次,右侧改名后为人次;两表仍有相同的四条城市年度记录和相同数值

rename() 只改变列名,观测与数值保持不变

改名之后要使用新名称。 renamed_data 中人口列叫 population;practice_data 中仍叫 pop,因为结果保存到了另一个对象。

筛选:filter()
3.2.4 条件筛选

filter() 选择数据的行,只保留条件结果为 TRUE 的记录。下例同时限定年份和大洲,最后显示前三条结果:

practice_data %>%
  filter(year == 2007, continent == "Europe") %>%
  select(country, year, life_exp) %>%
  head(3)
# A tibble: 3 × 3
  country  year life_exp
  <fct>   <int>    <dbl>
1 Albania  2007     76.4
2 Austria  2007     79.8
3 Belgium  2007     79.4
左侧城市年度表中两条 2020 年记录被虚线标出,右侧筛选结果保留 A 市和 B 市的 2020 年记录及三个原变量

filter() 按条件保留观测,变量列保持不变

filter() 中常用的条件写法
目的 条件写法
等于、不等于 year == 2007;year != 2007
达到某个阈值 life_exp >= 75
同时满足 filter(year == 2007, life_exp >= 75)
满足至少一个 filter(year == 2007 \| life_exp >= 75)
属于指定类别 filter(continent %in% c("Asia", "Europe"))
位于闭区间 filter(between(year, 1997, 2007)),包含两端

表内条件用于 practice_data %>% 之后。判断相等使用 ==;字符取值如 "Europe" 需要引号。

交互观察
条件改变后,哪些行被保留?

在同一张成绩表上调整年份、分数和条件组合,观察筛选后的行数。

查看示例数据
示例数据:demo_data
idgroupyearscore
1A202055
2A202170
3B202070
4B202185
5C202085
6C202195
library(dplyr)

# 教学模拟数据:每行是一名学生的一次成绩记录
demo_data <- tibble::tibble(
  id = 1:6,
  group = c("A", "A", "B", "B", "C", "C"),
  year = c(2020, 2021, 2020, 2021, 2020, 2021),
  score = c(55, 70, 70, 85, 85, 95)
)

demo_data %>%
  filter(year == 2020, score >= 60)
当前设置的运行结果
idgroupyearscore
3B202070
5C202085

年份和分数两个条件都为 TRUE 的行才被保留。 结果:2 行 × 4 列。

预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。

条件为 NA 的行也不会保留。 filter(!is.na(life_exp)) 明确删除寿命缺失的记录。若希望保留“寿命至少 75 或寿命缺失”的记录,条件应写为 life_exp >= 75 | is.na(life_exp)。缺失值的处理决定需要结合分析目的。

选学:按文本模式筛选

str_detect() 检查单元格的文本是否符合模式。例如:

practice_data %>%
  filter(year == 2007, str_detect(country, "^United")) %>%
  select(country, year)
# A tibble: 2 × 2
  country         year
  <fct>          <int>
1 United Kingdom  2007
2 United States   2007

^United 表示以 United 开头。这属于模式匹配,不会自动识别拼写错误或同义名称。选择列名的 matches() 与检查列内取值的 str_detect() 作用位置不同。

排序:arrange()
3.2.5 数据排序

arrange() 默认按升序排列,desc() 指定降序。多个排序变量依次生效:先比较第一列,取值相同时再比较下一列。排序改变行的位置,不会自动生成排名变量。

sorted_data <- practice_data %>%
  filter(year == 2007) %>%
  arrange(continent, desc(life_exp))

sorted_data %>%
  select(country, continent, life_exp) %>%
  head(5)
# A tibble: 5 × 3
  country   continent life_exp
  <fct>     <fct>        <dbl>
1 Reunion   Africa        76.4
2 Libya     Africa        74.0
3 Tunisia   Africa        73.9
4 Mauritius Africa        72.8
5 Algeria   Africa        72.3

结果先按大洲排列,再在各洲内部按寿命降序排列。sorted_data 保存排序后的表;原 practice_data 保持不变。

左侧四条城市年度记录顺序杂乱,右侧按到馆人次从 120、110、95 到 90 降序排列;每条记录的城市、年份和数值始终一起移动

arrange() 按到馆人次降序重排记录,整行数据一起移动

本地数据框中的 NA 排在最后,使用 desc() 时也如此。字符排序受语言区域设置影响;中文姓名或地区名不宜直接解释为“按拼音顺序”。

取行:slice_*()
3.2.6 取行与抽样

取出表格开头的记录、取出指标最高的记录和随机抽取记录,分别服务于预览、极值比较和抽样。slice_*() 用不同函数区分这些操作。

取行与随机抽样的常用函数
函数 常用参数 含义
slice_head() n = 3 按当前顺序取前三行
slice_max() order_by、n、with_ties 按指定变量取最大值记录
slice_sample() n 或 prop、replace 随机抽取记录
左侧为四条城市年度记录,右侧四个面板分别展示保留第 2 至 3 行、前 2 行、随机 2 行和到馆人次最大的 2 行

slice 系列函数按行位置、表头位置、指标大小或随机方式取得记录

practice_data %>%
  filter(year == 2007) %>%
  slice_max(order_by = life_exp, n = 3, with_ties = FALSE) %>%
  select(country, life_exp)
# A tibble: 3 × 2
  country          life_exp
  <fct>               <dbl>
1 Japan                82.6
2 Hong Kong, China     82.2
3 Iceland              81.8

order_by = life_exp 指定比较依据。with_ties 默认是 TRUE,边界上的并列值会一并保留;设为 FALSE 时至多取指定数量,同分记录按输入顺序选取。

随机种子(random seed)确定伪随机序列的起点。 set.seed() 属于基础 R,通常放在需要复现的随机操作之前:

set.seed(123)

sampled_countries <- practice_data %>%
  filter(year == 2007) %>%
  slice_sample(n = 5, replace = FALSE)

sampled_countries %>% select(country, year, life_exp)
# A tibble: 5 × 3
  country       year life_exp
  <fct>        <int>    <dbl>
1 Botswana      2007     50.7
2 Greece        2007     79.5
3 South Africa  2007     49.3
4 Ethiopia      2007     52.9
5 Zimbabwe      2007     43.5

replace = FALSE 是默认值,表示不放回抽样;TRUE 允许同一条记录被抽中多次。按比例抽取可写 slice_sample(prop = 0.2),与 n 二选一;行数取整时向下舍去不足一行的部分。先限定年份后,一行对应一个国家该年的记录;直接抽取整个数据集,抽到的则是国家年度记录。

交互观察
抽取数量、并列值与随机种子

切换抽取方式,比较相同数据在不同设置下的结果。随机模式提供连续 5 次抽取,随后可从头重跑。

查看示例数据
示例数据:demo_data
idgroupyearscore
1A202055
2A202170
3B202070
4B202185
5C202085
6C202195
n 与 prop 二选一;两者都省略时默认为 1 行。
从头重跑会重新设置种子。
默认不放回;放回抽样允许同一条记录重复出现。
默认保留并列值,因此结果可能超过指定行数。
library(dplyr)

# 教学模拟数据:每行是一名学生的一次成绩记录
demo_data <- tibble::tibble(
  id = 1:6,
  group = c("A", "A", "B", "B", "C", "C"),
  year = c(2020, 2021, 2020, 2021, 2020, 2021),
  score = c(55, 70, 70, 85, 85, 95)
)

set.seed(123)
demo_data %>%
  slice_sample(n = 3, replace = FALSE)
当前设置的运行结果
idgroupyearscore
3B202070
6C202195
2A202170

当前为同一种子起点后的第 1 次抽取。继续抽取会推进随机数状态;从头重跑可复现第 1 次结果。本次抽取中每条记录最多出现一次。 结果:3 行 × 4 列。

预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。抽样预览使用 R 4.6.1、dplyr 1.2.1。随机结果还取决于数据顺序与随机数生成器设置。

固定起点,复现操作序列。 在相同数据顺序、代码和随机数设置下,重新运行 set.seed(123) 及其后的抽样代码,可以复现结果。只设置一次种子后继续抽取,随机数状态会向前推进,结果通常发生变化。

种子不保证样本具有代表性。研究对象是否被覆盖、抽样方式是否合适,仍需要结合数据来源判断。不同种子也可能偶然抽到相同样本。

选学:同一种子、连续抽取与分组抽样
set.seed(123)
first_sample <- practice_data %>% slice_sample(n = 4)
next_sample <- practice_data %>% slice_sample(n = 4)

set.seed(123)
replayed_sample <- practice_data %>% slice_sample(n = 4)
identical(first_sample, replayed_sample)
[1] TRUE

最后输出 TRUE,因为第二次设置种子恢复了相同起点。next_sample 接着第一次抽取后的状态运行,不能预期它与第一次相同。复现完整分析时,通常在随机流程起点设置种子,并保留后续操作顺序;无需机械地在每一步前重设。

已经分组的数据会在每组内部抽取:

set.seed(123)
practice_data %>%
  filter(year == 2007) %>%
  group_by(continent) %>%
  slice_sample(n = 1) %>%
  ungroup() %>%
  select(continent, country)
# A tibble: 5 × 2
  continent country         
  <fct>     <fct>           
1 Africa    Mauritania      
2 Americas  Jamaica         
3 Asia      Korea, Dem. Rep.
4 Europe    Belgium         
5 Oceania   New Zealand     

这得到每洲一条记录,与从全部国家中抽五条不同。正式复现还应记录 R 和包版本;本节不展开随机数生成器的底层算法。

构造:mutate()
3.2.7 变量构造

mutate() 根据已有列计算新列,例如单位换算、差值或分类标签。普通数据框的行数保持不变。结果使用已有列名时,该列会被更新;使用新名称时,会增加一列。

derived_data <- practice_data %>%
  mutate(
    pop_million = pop / 1000000,
    life_exp_group = if_else(life_exp >= 75, "至少75岁", "低于75岁")
  )

derived_data %>%
  select(country, year, pop_million, life_exp_group) %>%
  head(3)
# A tibble: 3 × 4
  country      year pop_million life_exp_group
  <fct>       <int>       <dbl> <chr>         
1 Afghanistan  1952        8.43 低于75岁      
2 Afghanistan  1957        9.24 低于75岁      
3 Afghanistan  1962       10.3  低于75岁      

pop_million 以百万人为单位,计算时保留精度。if_else() 的三个主要参数依次是条件、成立时的值、不成立时的值;两种结果需要具有兼容的类型。条件缺失时,默认结果也是缺失。

左侧表格记录四条城市年度到馆人次,右侧在原三列之后新增到馆人次千人次列,数值由原万人次乘以 10 得到

mutate() 根据已有变量计算新变量,同时保留原有记录

交互观察
阈值如何改变分类结果?

根据分数生成分类标签,比较新增一列与更新已有列。

查看示例数据
示例数据:demo_data
idgroupyearscorestatus
1A202055未评定
2A202170未评定
3B202070未评定
4B202185未评定
5C202085未评定
6C202195未评定
library(dplyr)

# 教学模拟数据:每行是一名学生的一次成绩记录
demo_data <- tibble::tibble(
  id = 1:6,
  group = c("A", "A", "B", "B", "C", "C"),
  year = c(2020, 2021, 2020, 2021, 2020, 2021),
  score = c(55, 70, 70, 85, 85, 95)
)
demo_data <- demo_data %>%
  mutate(status = "未评定")

demo_data %>%
  mutate(score_group = if_else(
    score >= 60, "达标", "待提升"
  ))
当前设置的运行结果
idgroupyearscorestatusscore_group
1A202055未评定待提升
2A202170未评定达标
3B202070未评定达标
4B202185未评定达标
5C202085未评定达标
6C202195未评定达标

分数达到 60 的记录标为“达标”。返回结果新增 score_group,已有 status 保持原值。当前代码没有赋值,demo_data 本身保持不变。 结果:6 行 × 6 列。

预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。

选学:多类标签与批量计算

case_when() 按顺序检查多条条件,采用第一条成立的结果。因此,重叠条件需要安排好先后顺序。

practice_data %>%
  filter(year == 2007) %>%
  mutate(life_exp_band = case_when(
    life_exp < 60 ~ "低于60岁",
    life_exp < 75 ~ "60至不足75岁",
    life_exp >= 75 ~ "至少75岁",
    .default = "缺失"
  )) %>%
  select(country, life_exp, life_exp_band) %>%
  head(3)
# A tibble: 3 × 3
  country     life_exp life_exp_band
  <fct>          <dbl> <chr>        
1 Afghanistan     43.8 低于60岁     
2 Albania         76.4 至少75岁     
3 Algeria         72.3 60至不足75岁 

across() 可以把同一种计算应用到指定列。下面用一个函数,将两列数值分别保留一位小数:

practice_data %>%
  mutate(across(c(life_exp, gdp_per_cap), function(x) round(x, 1))) %>%
  head(3)
# A tibble: 3 × 6
  country     continent  year life_exp      pop gdp_per_cap
  <fct>       <fct>     <int>    <dbl>    <int>       <dbl>
1 Afghanistan Asia       1952     28.8  8425333        779.
2 Afghanistan Asia       1957     30.3  9240934        821.
3 Afghanistan Asia       1962     32   10267083        853.

四舍五入适合整理展示结果,原始测量值通常另行保留。

选学:为国家名称匹配标准代号

countrycode 是额外的包,首次使用前在 Console 安装。通过 countrycode::countrycode() 指定函数来源,无需再加载该包。下面保存转换结果,再检查无法匹配的名称:

country_codes <- practice_data %>%
  mutate(iso3 = countrycode::countrycode(
    country, origin = "country.name", destination = "iso3c"
  ))

country_codes %>%
  filter(is.na(iso3)) %>%
  select(country) %>%
  distinct()
# A tibble: 0 × 1
# ℹ 1 variable: country <fct>

未匹配时应核对名称、地区编码及历史口径,再决定是否建立人工对照表。名称相似不保证指向同一研究对象。

汇总:group_by()
3.2.8 分组汇总

group_by() 指定分组变量,使后续计算分别在组内进行;它本身不把多条记录合成一条。summarise() 才将每组汇总成一行。

下面比较 2007 年各洲所含国家的出生时预期寿命:

continent_summary <- practice_data %>%
  filter(year == 2007) %>%
  group_by(continent) %>%
  summarise(
    n_country = n_distinct(country),
    life_exp_mean = mean(life_exp),
    .groups = "drop"
  )

continent_summary
# A tibble: 5 × 3
  continent n_country life_exp_mean
  <fct>         <int>         <dbl>
1 Africa           52          54.8
2 Americas         25          73.6
3 Asia             33          70.7
4 Europe           30          77.6
5 Oceania           2          80.7

每个国家在当年只有一条记录,所以均值为国家之间的未加权均值,不能直接解释为全洲人口的平均寿命。.groups = "drop" 取消结果中的分组状态,避免后续操作继续按旧分组执行。

左侧是 A 市和 B 市各两年的到馆人次记录,中间代码按城市分组并计算均值与观测数,右侧结果每个城市一行,均值分别为 115 和 92.5,观测数均为 2

group_by() 指定城市分组,summarise() 为每组生成一行汇总结果

分组汇总中常见统计量的含义
统计目的 函数 需要辨认的含义
数记录 n() 统计组内行数
数不同对象 n_distinct(country) 同一国家出现多次只计一次
算均值、中位数 mean()、median() 明确哪些记录参与计算
求总和 sum(pop) 总和仅覆盖数据集所含对象
交互观察
分组条件决定每一行汇总的含义

选择分组变量与统计量,比较原始记录和汇总结果的行数。

查看示例数据
示例数据:demo_data
idgroupyearscore
1A202055
2A202170
3B202070
4B202185
5C202085
6C202195
library(dplyr)

# 教学模拟数据:每行是一名学生的一次成绩记录
demo_data <- tibble::tibble(
  id = 1:6,
  group = c("A", "A", "B", "B", "C", "C"),
  year = c(2020, 2021, 2020, 2021, 2020, 2021),
  score = c(55, 70, 70, 85, 85, 95)
)

demo_data %>%
  group_by(group) %>%
  summarise(mean_score = mean(score), .groups = "drop")
当前设置的运行结果
groupmean_score
A62.5
B77.5
C90

每行对应一个小组。同组内的记录参与同一次计算。结果已取消分组。 结果:3 行 × 2 列。

预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。

多个变量可以共同分组。 按大洲与年份计算时,每行结果表示某洲在某年的国家均值:

practice_data %>%
  group_by(continent, year) %>%
  summarise(
    life_exp_mean = mean(life_exp),
    .groups = "drop"
  ) %>%
  head(4)
# A tibble: 4 × 3
  continent  year life_exp_mean
  <fct>     <int>         <dbl>
1 Africa     1952          39.1
2 Africa     1957          41.3
3 Africa     1962          43.3
4 Africa     1967          45.3

只需要计数时,count() 更紧凑:

practice_data %>%
  filter(year == 2007) %>%
  count(continent, name = "n_country")
# A tibble: 5 × 2
  continent n_country
  <fct>         <int>
1 Africa           52
2 Americas         25
3 Asia             33
4 Europe           30
5 Oceania           2

先确认统计单位,再解读结果。 对全部年份运行 count(continent) 数的是国家年度记录。若每行不是一个独立国家,就不能把行数称为国家数。遇到缺失值时,na.rm = TRUE 表示计算时排除缺失值;使用前应说明缺失数量及其影响。

选学:汇总后连接回原记录,构建相对指标

可先计算每年的国家均值,再连接回国家年度表。这里以 100 表示等于当年所含国家的未加权均值:

year_benchmark <- practice_data %>%
  group_by(year) %>%
  summarise(
    country_mean = mean(life_exp),
    .groups = "drop"
  )

relative_data <- practice_data %>%
  left_join(year_benchmark, by = "year", relationship = "many-to-one") %>%
  mutate(life_exp_index = life_exp / country_mean * 100)

relative_data %>%
  filter(year == 2007) %>%
  select(country, life_exp, country_mean, life_exp_index) %>%
  head(3)
# A tibble: 3 × 4
  country     life_exp country_mean life_exp_index
  <fct>          <dbl>        <dbl>          <dbl>
1 Afghanistan     43.8         67.0           65.4
2 Albania         76.4         67.0          114. 
3 Algeria         72.3         67.0          108. 

该指标只是相对比较,不代表排名或人口加权结果。原表的国家与年份组合已经唯一,无需再次按这两列求均值。

关联:left_join()
3.2.9 关联补充

left_join() 依据匹配字段(key)连接两张表。左表记录至少保留一次;右表匹配成功的信息写入新列,未匹配处填为 NA。

下例用一张教学模拟的展示分组表为国家记录补充标签,分组没有现实分类含义:

display_lookup <- tibble::tibble(
  country = c("China", "Japan", "Canada"),
  display_group = c("A组", "A组", "B组")
)

joined_data <- practice_data %>%
  left_join(display_lookup, by = "country", relationship = "many-to-one")

joined_data %>%
  filter(year == 2007, country %in% c("China", "Canada", "France")) %>%
  select(country, year, display_group)
# A tibble: 3 × 3
  country  year display_group
  <chr>   <int> <chr>        
1 Canada   2007 B组          
2 China    2007 A组          
3 France   2007 <NA>         

by = "country" 指定匹配列。relationship = "many-to-one" 声明每条左表记录最多匹配一条右表记录;右表若出现导致多重匹配的重复国家名称,R 会报错,便于发现问题。法国不在示例对照表中,因此其标签为 NA,这不代表属于某个相反类别。

左侧到馆表包含 A、B、C 三座城市,城市信息表只含 A、B;右侧连接结果保留全部五条到馆记录,A 和 B 获得区域类型,C 的区域类型为 NA,并展示 anti_join 找出 C 市

left_join() 保留左表记录并按城市补充区域类型,未匹配处为 NA

交互观察
匹配字段与重复记录怎样影响连接?

两张表均记录城市年度信息。先用城市与年份共同匹配,再观察只按城市匹配的结果。

查看示例数据
示例数据:visits_data
cityyearvisits
A2019120
A2020110
B201995
B202090
示例数据:budget_data
cityyearbudget
A201930
A202032
B201925
B202028
library(dplyr)
# 教学模拟数据;visits 为万人次,budget 为万元
visits_data <- tibble::tibble(
  city = c("A", "A", "B", "B"),
  year = c(2019, 2020, 2019, 2020),
  visits = c(120, 110, 95, 90)
)
budget_data <- tibble::tibble(
  city = c("A", "A", "B", "B"),
  year = c(2019, 2020, 2019, 2020),
  budget = c(30, 32, 25, 28)
)

visits_data %>%
  left_join(budget_data, by = c("city", "year"))
当前设置的运行结果
cityyearvisitsbudget
A201912030
A202011032
B20199525
B20209028

城市与年份共同确定年度记录。本例一对一匹配,4 条到馆记录分别取得对应预算。 结果:4 行 × 4 列。

预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。

检查匹配关系,不能只看是否运行成功。 右表某个 key 重复,可能使左表的一条记录匹配多行,形成一对多;两边对应 key 都重复时,可能形成多对多。连接后应检查行数、关键字段及未匹配记录。

字符与因子等部分类型可以兼容,字符与数值等不兼容类型通常会报错。转换类型前还要检查前导零、空格与字段含义,避免把不同编号误当成相同值。

# 连接前后行数应符合预期
c(before = nrow(practice_data), after = nrow(joined_data))
before  after 
  1704   1704 
# 检查对照表中不存在的国家
practice_data %>%
  filter(year == 2007) %>%
  anti_join(display_lookup, by = "country") %>%
  select(country) %>%
  head(3)
# A tibble: 3 × 1
  country    
  <fct>      
1 Afghanistan
2 Albania    
3 Algeria    
选学:连接方式、不同列名与名称匹配
根据记录保留范围选择连接方式
目的 函数
保留两边都匹配的记录 inner_join()
保留两表全部记录 full_join()
按右表保留记录 right_join()
找出左表未匹配记录 anti_join()
仅筛出左表能匹配的记录,不添加右表列 semi_join()

匹配字段名称不同时,可写 by = c("country" = "country_name"),左侧是左表列名,右侧是右表列名。城市年度数据通常还需加入年份,避免跨年错配。实际记录的 key 缺失时也应先检查;本地等值连接默认允许 NA 与 NA 匹配。

名称不一致时,先核对拼写、空格、简称和正式编码。模糊匹配(fuzzy matching)可以提供相似候选,但简称与全称不一定具有较高的字符相似度,匹配后仍需人工核验。入门阶段先掌握明确 key 的连接,延伸方法见 R for Data Science:Joins。

追加:bind_rows()
3.2.10 按行追加

不同年份或调查批次的数据,如果每行记录的含义一致,可以使用 bind_rows() 叠放到同一张表中。它按列名对齐,不会依据 key 查找对应记录,也不会自动去重。

data_2002 <- practice_data %>% filter(year == 2002)
data_2007 <- practice_data %>% filter(year == 2007)

combined_data <- bind_rows(
  "2002_batch" = data_2002,
  "2007_batch" = data_2007,
  .id = "source_batch"
)

combined_data %>% count(source_batch)
# A tibble: 2 × 2
  source_batch     n
  <chr>        <int>
1 2002_batch     142
2 2007_batch     142
左侧上表包含 A 市和 B 市的 2019 年记录,下表包含两市的 2020 年记录;右侧结果将四条记录上下堆叠,列名和变量含义保持一致

bind_rows() 将列结构一致的 2019 年与 2020 年记录按行追加

.id = "source_batch" 新增一列记录来源。某一批数据缺少的列会补为 NA;同名列的类型需要兼容。追加前应核对变量含义、计量单位和编码规则,列名相同并不保证口径一致。

选学:按行追加、按 key 连接与按位置拼列

bind_rows() 增加记录,left_join() 按 key 补充信息。bind_cols() 则直接按当前行位置拼列,不检查同一行是否属于同一个对象。只有两边行数、顺序与记录含义均已确认对应时,才适合按位置拼列。

拆合:separate_wider_delim()
3.2.11 字段拆合

当同一列按固定规则合存多个变量时,separate_wider_delim() 可以按分隔符拆列。下例的 profile 依次保存姓名代号、性别代码和年龄,其中 M、F 分别表示男性、女性。

messy_people <- tibble::tibble(
  id = 1:4,
  profile = c("Li_M_23", "Wang_F_19", "Zhang_M_31", "Chen_F_26")
)

clean_people <- messy_people %>%
  separate_wider_delim(
    cols = profile, delim = "_",
    names = c("name", "sex", "age")
  ) %>%
  mutate(age = readr::parse_integer(age))

clean_people
# A tibble: 4 × 4
     id name  sex     age
  <int> <chr> <chr> <int>
1     1 Li    M        23
2     2 Wang  F        19
3     3 Zhang M        31
4     4 Chen  F        26

cols 指定待拆列,delim 指定固定分隔符,names 依次命名结果列。拆分后的年龄起初是字符,parse_integer() 将其解析为整数。某条记录的片段数与预期不符时,默认会报错,应先检查原始记录。

反过来,unite() 按指定分隔符把多列连接成字符:

clean_people %>%
  unite(profile_text, name, sex, age, sep = "_", remove = FALSE)
# A tibble: 4 × 5
     id profile_text name  sex     age
  <int> <chr>        <chr> <chr> <int>
1     1 Li_M_23      Li    M        23
2     2 Wang_F_19    Wang  F        19
3     3 Zhang_M_31   Zhang M        31
4     4 Chen_F_26    Chen  F        26

remove = FALSE 保留参与拼接的原列;默认 TRUE 会在返回结果中移除这些列。这里的 profile_text 用于展示,分析时仍可直接使用独立的姓名、性别和年龄列。

上半部分把 location 中以竖线分隔的国家与地区拆成两列;下半部分把国家和地区重新合并为 location,四条城市记录保持对应

separate_wider_delim() 按分隔符拆列,unite() 按指定分隔符合并多列

选学:旧函数与日期时间的处理边界

旧函数 separate() 仍可运行,但官方已标记为 superseded。按固定分隔符拆分的新代码优先使用 separate_wider_delim();两者参数名称和分隔规则有区别,不能只替换函数名。

拼接日期各部分得到的仍是字符。若需要时间计算,可直接构造日期时间对象:

time_parts <- tibble::tibble(
  year = c(2025, 2025), month = c(4, 4), day = c(29, 30),
  hour = c(19, 8), minute = c(3, 45), second = c(52, 10)
)

time_parts %>%
  mutate(timestamp = lubridate::make_datetime(
    year, month, day, hour, minute, second, tz = "Asia/Shanghai"
  ))
# A tibble: 2 × 7
   year month   day  hour minute second timestamp          
  <dbl> <dbl> <dbl> <dbl>  <dbl>  <dbl> <dttm>             
1  2025     4    29    19      3     52 2025-04-29 19:03:52
2  2025     4    30     8     45     10 2025-04-30 08:45:10
转换:pivot_*()
3.2.12 长宽转换

pivot_longer() 把多列测量值整理为名称列和值列;pivot_wider() 将名称列的取值展开为列名。选择排法时,需要明确一行代表什么,以及后续计算所需的结构。

下面用两座城市的年度到馆人次演示,单位为万人次,均为模拟数据:

wide_data <- tibble::tibble(
  city = c("A", "B"),
  `2019` = c(120, 95),
  `2020` = c(110, 90)
)

wide_data
# A tibble: 2 × 3
  city  `2019` `2020`
  <chr>  <dbl>  <dbl>
1 A        120    110
2 B         95     90

宽表转长表:让年份进入一列。

long_data <- wide_data %>%
  pivot_longer(
    cols = c(`2019`, `2020`),
    names_to = "year",
    values_to = "visits",
    names_transform = list(year = as.integer)
  )

long_data
# A tibble: 4 × 3
  city   year visits
  <chr> <int>  <dbl>
1 A      2019    120
2 A      2020    110
3 B      2019     95
4 B      2020     90

cols 指定需要收集的年份列,names_to 接收原列名,values_to 接收原单元格数值。names_transform 把年份转换为整数。结果为四行,每行表示一条城市年度记录,四个到馆人次数值都保留。

长表转宽表:把年份取值展开为列名。

wide_again <- long_data %>%
  pivot_wider(
    id_cols = city,
    names_from = year,
    values_from = visits
  )

wide_again
# A tibble: 2 × 3
  city  `2019` `2020`
  <chr>  <dbl>  <dbl>
1 A        120    110
2 B         95     90

id_cols = city 表示宽表中每个城市占一行。names_from 指定新列名取自年份,values_from 指定单元格填入到馆人次。

上半部分把 2019 至 2021 三个年份列转换为年份和人数两列,每个城市年度成为一行;下半部分将长表中的年份取值重新展开为三个年份列

pivot_longer() 将年份列整理为名称和值两列,pivot_wider() 执行相反方向的转换

交互观察
列名与数值在长宽转换中去向何处?

使用同一组城市到馆人次,比较长表与宽表。数值单位为万人次,均为教学模拟数据。

查看示例数据
示例数据:wide_data
city20192020
A120110
B9590
默认不添加前缀。
library(tidyverse)
wide_data <- tibble::tibble(
  city = c("A", "B"),
  `2019` = c(120, 95),
  `2020` = c(110, 90)
)

wide_data %>%
  pivot_longer(
    cols = c(`2019`, `2020`),
    names_to = "year", values_to = "visits",
    names_transform = list(year = as.integer)
  )
当前设置的运行结果
cityyearvisits
A2019120
A2020110
B201995
B202090

原列名中的年份进入 year;四个到馆人次数值进入 visits。结果为 4 行,每行一条城市年度记录。 结果:4 行 × 3 列。

预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。

同一个结果位置应对应一个值。 若同一城市同一年存在多条到馆记录,pivot_wider() 无法直接决定填哪一个值,可能提示并生成列表列。应先核对观测单位和重复原因;只有确实需要汇总时,才通过 values_fn 指定计算,不能用求均值掩盖重复问题。

长表与宽表都可能适合分析。不同独立变量各占一列的宽表可以符合整洁规则;年份作为变量时,将年份取值单独列出通常更便于按年筛选和分组。转换应服务于变量含义和分析任务。

3.3 自测与资源
01运行 practice_data %>% filter(year == 2007) 后,原对象会怎样?

02在相同数据顺序与运行环境下,哪种操作可以复现第一次抽样?

03按大洲分组后计算 mean(life_exp),每行汇总结果表示什么?

函数文档与延伸阅读

4.0 数据清洗与诊断

4.0 数据清洗与诊断

清洗主题

清洗导航

从质量诊断到可核对的清洗结果

数据清洗(data cleaning)用于识别并处理会妨碍计算或扭曲解释的数据问题。整洁数据规定变量、观测与取值如何排列表格;质量诊断进一步检查记录是否缺失、重复、误编码或命名不一致。

  1. 01明确规则写清观测单位、key、变量含义与允许取值。
  2. 02量化问题统计缺失、重复、类型与异常编码。
  3. 03选择处理依据采集过程和分析目标决定删除、修正或保留。
  4. 04复核变化比较处理前后的行数、缺失数、类型与 key 唯一性。

清洗时应保留原始文件,将处理结果写入新对象,并记录每一步的判断依据。这样才能发现误删、误填或单位转换错误。

从空会话运行本节代码

先加载 tidyverse。其余示例会在使用对象前创建对象;janitor 仅在 4.4 通过 janitor::clean_names() 调用。

library(tidyverse)
缺失值:NA
4.1 识别与处理缺失值

R 使用 NA 表示缺失值(missing value)。NA 说明某个取值未知或未记录,但没有说明缺失原因。它也不同于数值 0、空字符串 "" 和文本 "NA";后两者仍是普通字符,读取数据时未必会自动转换为真正的缺失值。

常见空值形式及其含义
数据中的形式 R 的理解 处理前需要确认的事项
NA 缺失值 缺失如何产生,是否影响当前分析变量
0 数值零 零是否表示真实的“没有”或“未发生”
"" 长度为零的文本 空白是否应重编码为 NA
"NA"、"未知" 普通文本标签 数据字典是否把这些标签定义为缺失
4.1.1 定位与统计

is.na() 对每个元素进行判断,anyNA() 只回答对象中是否存在缺失值。需要统计数量时,可对 is.na() 的逻辑结果求和。判断缺失值应写作 is.na(visits);表达式 visits == NA 的比较结果仍是 NA,无法筛出缺失记录。

缺失值诊断中常用的四种写法
任务 写法 返回内容
判断对象是否含缺失值 anyNA(data) 一个 TRUE 或 FALSE
逐项标记缺失位置 is.na(data) 与原对象对应的逻辑结果
统计某列缺失数量 sum(is.na(data$visits)) 一个计数值
查看某列缺失的记录 filter(data, is.na(visits)) 满足条件的行
# 教学模拟数据:每行是一座城市在一个年份的记录
missing_data <- tibble::tibble(
  city = c("A 市", "A 市", "B 市", "B 市"),
  year = c(2019, 2020, 2019, 2020),
  visits = c(120, NA, 100, 80),
  status = c("开放", "开放", NA, "开放")
)

# 数据中是否存在缺失值
anyNA(missing_data)
  [1] TRUE
# 分别统计两个变量中的缺失数量
missing_data %>%
  summarise(
    n_missing_visits = sum(is.na(visits)),
    n_missing_status = sum(is.na(status))
  )
  # A tibble: 1 × 2
    n_missing_visits n_missing_status
               <int>            <int>
  1                1                1
# 查看 visits 缺失的记录
missing_data %>%
  filter(is.na(visits))
  # A tibble: 1 × 4
    city   year visits status
    <chr> <dbl>  <dbl> <chr> 
  1 A 市   2020     NA 开放

na.rm = TRUE 只改变当前计算

许多统计函数遇到 NA 时会返回 NA。参数 na.rm = TRUE 表示在当前统计量中排除缺失观测;它不会填补或删除原数据中的缺失值。排除后,有效样本量和分母可能改变,因此结果应同时说明参与计算的记录数。

mean(c(1, 2, NA, 4))
  [1] NA
mean(c(1, 2, NA, 4), na.rm = TRUE)
  [1] 2.333333
4.1.2 选择处理方式

处理缺失值前,需要结合数据来源、变量用途和缺失机制决定方案。删除会改变分析样本;填补会加入原本没有观测到的数值;保留缺失则要求后续函数能够明确处理 NA。

城市年度数据示意图:左侧原表含一个到馆人次缺失值,右侧分别展示 drop_na 删除该行和 replace_na 使用均值 100 填补后的结果

缺失记录删除与均值填补会改变样本行数或数值分布

图中的 original_data 代表尚未清洗的原表。均值填补用于比较两种操作产生的差异,不代表正式分析中的默认选择。

三类缺失值处理方式的用途与边界
处理方式 常用操作 适用条件与影响
删除记录 drop_na() 删除含指定缺失值的行;需检查样本构成与样本量变化
填入明确类别或数值 replace_na() 只有数据字典或研究设计能够支持该替代值时才使用
沿相邻记录补齐 fill() 适合组内本应重复记录的属性;必须先分组、排序并核对时间连续性
# 仅删除 visits 缺失的城市年度记录
missing_data %>%
  drop_na(visits)
  # A tibble: 3 × 4
    city   year visits status
    <chr> <dbl>  <dbl> <chr> 
  1 A 市   2019    120 开放  
  2 B 市   2019    100 <NA>  
  3 B 市   2020     80 开放
# 教学比较:用非缺失值的均值生成一个新变量
missing_data %>%
  mutate(
    visits_mean_demo = replace_na(
      visits,
      mean(visits, na.rm = TRUE)
    ),
    status = replace_na(status, "未记录")
  )
  # A tibble: 4 × 5
    city   year visits status visits_mean_demo
    <chr> <dbl>  <dbl> <chr>             <dbl>
  1 A 市   2019    120 开放                120
  2 A 市   2020     NA 开放                100
  3 B 市   2019    100 未记录              100
  4 B 市   2020     80 开放                 80

固定值填补只有在缺失含义明确时才成立。“未填写”不能直接解释为“无”或 0。均值、中位数和单次模型填补都会改变变量分布,并低估填补带来的不确定性。

选学:缺失机制与进阶处理的边界
  • 缺失完全随机(missing completely at random, MCAR)要求缺失概率与已观测和未观测信息都无关,这一条件通常需要结合采集过程判断。
  • 模型填补利用其他变量预测缺失值。单次预测会把一个估计值当作已知值,不能反映填补不确定性。
  • 多重插补(multiple imputation)生成多份合理的完整数据并合并估计,更适合正式推断,但需要单独学习模型设定与诊断。
  • 时间序列插值依赖局部连续性。观测间隔不规则、缺失区间较长或发生结构变化时,平滑结果可能掩盖真实变化。
重复记录:distinct()
4.2 识别重复记录

重复记录(duplicate records)可能来自重复导入、一对多连接,也可能是同一对象的多次有效测量。判定重复之前必须明确观测单位和 key。若每行表示“一座城市在一个年份的记录”,则 city + year 才能共同标识一条城市年度观测;同一城市出现在多个年份是合理现象。

三种需要分开的情况

  • 整行重复:所有变量完全相同,常见于重复导入。
  • key 重复:应当唯一的 key 出现多次,但其他值可能不同,需要追查来源或制定保留规则。
  • 有效重复测量:同一对象在不同时间或条件下被多次记录,应保留区分这些观测的变量。
4.2.1 重复组与额外行

“重复记录数”至少有两种口径。nrow(data) - nrow(distinct(data)) 计算每组保留一行后多出的额外行数;filter(n() > 1) 返回属于重复组的全部记录。报告结果时应写明采用哪一种口径。

# 教学模拟数据:每行是一座城市在一个年份的到馆记录
duplicate_data <- tibble::tibble(
  city = c("A 市", "A 市", "A 市", "B 市", "B 市", "B 市"),
  year = c(2019, 2020, 2020, 2019, 2020, 2020),
  visits = c(120, 110, 110, 95, 90, 90)
)

# 定位整行重复组,并显示每组包含的记录数
duplicate_data %>%
  count(city, year, visits, name = "n") %>%
  filter(n > 1)
  # A tibble: 2 × 4
    city   year visits     n
    <chr> <dbl>  <dbl> <int>
  1 A 市   2020    110     2
  2 B 市   2020     90     2
# 计算每组只保留一行后,多出的额外行数
n_extra_rows <- nrow(duplicate_data) - nrow(distinct(duplicate_data))
n_extra_rows
  [1] 2
# 检查 city + year 这个 key 是否重复
duplicate_data %>%
  count(city, year, name = "n") %>%
  filter(n > 1)
  # A tibble: 2 × 3
    city   year     n
    <chr> <dbl> <int>
  1 A 市   2020     2
  2 B 市   2020     2
4.2.2 按 key 去重
城市年度数据示意图:上方 distinct 删除完全重复的行并保留四条城市年度记录,下方 distinct city 只保留每个城市首次出现的记录并丢失其他年份

整行去重与仅按城市去重产生的不同结果

图中上方的 distinct() 删除两条额外的整行副本,仍保留 4 条城市年度记录。下方仅按 city 去重后只剩 2 行,因为 key 与观测单位不一致;该分支用于说明错误选择 key 的后果。

交互观察
key 改变时,哪些城市年度记录会被保留?

同一张表分别按全部变量、城市与年份、仅城市判断重复。观察 key 与观测单位不一致时发生的数据丢失。

查看示例数据
示例数据:original_data
cityyearvisits
A 市2019120
A 市2020110
A 市2020110
B 市201995
B 市202090
B 市202090
key 应与一行所代表的观测单位一致。
library(dplyr)
# 教学模拟数据:每行是一座城市在一个年份的到馆记录
original_data <- tibble::tibble(
  city = c("A 市", "A 市", "A 市", "B 市", "B 市", "B 市"),
  year = c(2019, 2020, 2020, 2019, 2020, 2020),
  visits = c(120, 110, 110, 95, 90, 90)
)

original_data %>%
  distinct()
当前设置的运行结果
cityyearvisits
A 市2019120
A 市2020110
B 市201995
B 市202090

所有变量完全相同的副本被删除。A 市和 B 市的 2019、2020 年记录仍被保留。 结果:4 行 × 3 列。

预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。

.keep_all = TRUE 不会决定哪条记录更可信

distinct(city, year, .keep_all = TRUE) 对每个 key 保留当前排序中的第一行。若重复行的数值不同,应先追查差异来源,并按照日期、版本或质量标记写出可复核的保留规则。单纯改变排序只能改变保留结果,不能证明该记录正确。

格式与类型:stringr、readr
4.3 清理格式与编码

多源数据常把同一类别写成不同形式,例如 "上海" 与 " 上海 ";也可能把 "N/A"、"未知" 或空字符串保存在字符列中。货币符号、百分号和单位还会使本应参与计算的数值被读取为字符。清洗目标是让同一种含义采用同一种编码,并保留单位与转换规则。

字符与数值格式问题的常用处理
问题 常用函数 处理结果
首尾含空白 str_trim() 删除字符串两端的空白
内部连续空白 str_squish() 首尾去空白,并把内部连续空白压为一个空格
大小写混用 str_to_lower()、str_to_upper() 按数据字典统一大小写
文本型缺失编码 na_if() 将指定文本转换为真正的 NA
数字附带符号或单位 parse_number() 提取第一个可解析的数字,尺度仍需另行处理
# 教学模拟数据:文本、缺失编码和数字格式并存
format_data <- tibble::tibble(
  city = c(" 上海", "北京 ", "广州"),
  district = c("浦东  新区", "N/A", ""),
  income = c("¥2,500", "3,000 元", "4,200 元"),
  participation_rate = c("35%", "42%", "N/A")
)

clean_format_data <- format_data %>%
  mutate(
    city = str_trim(city),
    district = str_squish(district),
    district = na_if(district, "N/A"),
    district = na_if(district, ""),
    participation_rate = na_if(participation_rate, "N/A"),
    income_yuan = parse_number(income),
    participation_rate = parse_number(participation_rate) / 100
  )

clean_format_data
  # A tibble: 3 × 5
    city  district  income   participation_rate income_yuan
    <chr> <chr>     <chr>                 <dbl>       <dbl>
  1 上海  浦东 新区 ¥2,500                 0.35        2500
  2 北京  <NA>      3,000 元               0.42        3000
  3 广州  <NA>      4,200 元              NA           4200

parse_number() 提取数字,不判断尺度

parse_number("35%") 返回 35。若分析变量按 0 到 1 的比例保存,还需除以 100。同理,parse_number("12 公里") 返回 12,但不会保留“公里”这个单位,也不会把“12 万元”自动换算为 120000。单位和换算规则应写入变量说明并在清洗后核对。

列名规范:janitor
4.4 统一列名为 snake_case

含空格、括号和符号的列名需要用反引号引用,也容易在连续处理时写错。本课程采用 snake_case:英文小写单词之间使用下划线,例如 annual_income。这是课程和项目的统一约定,并非所有 R 项目的唯一规范。

janitor::clean_names() 可以批量规范列名。首次使用前,可在 Console 中运行 install.packages("janitor");安装完成后,Rmd 通过 包名::函数名() 直接调用函数。

clean_names() 对示例列名的实际转换结果
原列名 清洗后列名
First Name first_name
Annual Income ($) annual_income
% Change percent_change
bad_names_data <- tibble::tibble(
  `First Name` = "John",
  `Annual Income ($)` = 50000,
  `% Change` = 0.05
)

clean_names_data <- bad_names_data %>%
  janitor::clean_names()

names(clean_names_data)
  [1] "first_name"     "annual_income"  "percent_change"

清洗后应立即运行 names() 核对结果。后续代码必须使用新的列名;列名规范化不会改变单元格中的数据。

完成清洗后的最低检查

  • 比较处理前后的行数和列数,确认变化与预期一致。
  • 重新统计关键变量的缺失值,并检查变量类型、单位与允许范围。
  • 使用分析所需的 key 检查唯一性;重复测量数据还应确认时间或条件变量仍被保留。
  • 抽查若干原始记录与清洗结果,并保存清洗代码和判断依据。
4.5 自测与资源
01运行 mean(c(10, 20, NA, 30), na.rm = TRUE) 后,哪项解释正确?

02每行表示“一座城市在一个年份的记录”时,检查重复最合适的 key 是什么?

03若比例变量应保存为 0 到 1,parse_number("35%") 后还需进行哪一步?

函数文档与延伸阅读

5.0 探索性数据分析

5.0 探索性数据分析

重要

探索性数据分析 (Exploratory Data Analysis,EDA) 是一组用来认识数据、发现问题并形成后续分析判断的检查活动。统计学家 John W. Tukey 在 1977 年出版的 Exploratory Data Analysis 中系统阐述了这一思路:先观察数据呈现出的分布、差异和异常,再决定哪些统计方法与研究问题相匹配。

EDA 会出现在变量构造、建模和结果复核等多个阶段。一次检查可能暴露新的缺失、编码问题或组别差异;完成修正后,还需重新查看相关摘要。因此,EDA 通常是一个反复检查并记录判断的过程。

5.0.1 EDA 关注的问题

一份可复核的探索记录,至少应回答四类问题。

一行代表什么?
明确观测单位、时间范围和分析样本。例如,一行可能是一名受访者,也可能是一个国家在某一年的记录。
数据能否直接使用?
检查变量类型、缺失、重复、异常编码与合理范围。极端值需要核对来源,不能仅凭数值大小删除。
单个变量怎样分布?
查看中心位置、离散程度、偏态和极端记录。均值与标准差并非所有连续变量的固定搭配。
组别和变量之间有何关系?
比较各组构成与统计量,再检查变量关系的方向、形态和样本量。描述性差异本身不提供因果解释。

5.0.2 EDA 检查顺序

EDA 没有适用于所有研究的固定清单。下面的顺序适合社会科学数据的初次检查,也便于在分析日志中记录每一步的发现和处理决定。

  1. 确认分析单位写明一行记录谁或什么、时间点和纳入范围。
  2. 检查结构与完整性核对变量名、类型、总行数、有效值数和缺失数。
  3. 描述单变量根据分布选择均值与标准差,或中位数与四分位距。
  4. 比较关键组别检查组别样本量、构成比例和组内统计量。
  5. 检查变量关系结合关系形态、相关系数、完整观测数和研究背景解读。
  6. 记录异常与决定保存发现、处理依据及修正后的复查结果。

探索与验证的边界

查看数据后产生的新解释或假设,应在报告中标明为探索性发现。需要验证的研究问题通常还要结合预先设定的假设、独立数据或合适的研究设计。EDA 可以发现关联和数据问题,不能单独建立因果关系。

5.1 EDA 工具

工具导航

探索性数据分析工具

4 个入口依次完成数据概览、描述统计、分组比较和关系检查。

运行准备

本节继续使用 Gapminder。为使代码能够从空会话按顺序运行,先加载包并建立统一使用的分析对象。

library(tidyverse)
library(gapminder)

eda_data <- gapminder %>%
  transmute(
    country,
    continent,
    year,
    life_expectancy = lifeExp,
    population = pop,
    gdp_per_capita = gdpPercap
  )

eda_2007 <- eda_data %>%
  filter(year == 2007)

glimpse(eda_2007)
  Rows: 142
  Columns: 6
  $ country         <fct> "Afghanistan", "Albania", "Algeria", "Angola", "Argent…
  $ continent       <fct> Asia, Europe, Africa, Africa, Americas, Oceania, Europ…
  $ year            <int> 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, …
  $ life_expectancy <dbl> 43.828, 76.423, 72.301, 42.731, 75.320, 81.235, 79.829…
  $ population      <int> 31889923, 3600523, 33333216, 12420476, 40301927, 20434…
  $ gdp_per_capita  <dbl> 974.5803, 5937.0295, 6223.3675, 4797.2313, 12779.3796,…

准备块只调整本节使用的列名,没有改动 gapminder 原对象。后续输出中的一行表示“一个国家在一个年份的记录”;eda_2007 则固定为 2007 年截面。

数据概览
5.1.1 检查结构、范围与缺失

初次接触一份数据时,应先确认变量类型、取值范围和缺失情况。summary() 适合快速查看基础摘要;skimr::skim() 会把不同类型变量的统计量整理成结构化报告。

基础扫描:summary()

对数值变量,summary() 通常给出最小值、四分位数、中位数、均值和最大值;对分类变量,它会显示各类别的记录数。输出内容会随变量类型变化。

summary(eda_data)
          country        continent        year      life_expectancy
   Afghanistan:  12   Africa  :624   Min.   :1952   Min.   :23.60  
   Albania    :  12   Americas:300   1st Qu.:1966   1st Qu.:48.20  
   Algeria    :  12   Asia    :396   Median :1980   Median :60.71  
   Angola     :  12   Europe  :360   Mean   :1980   Mean   :59.47  
   Argentina  :  12   Oceania : 24   3rd Qu.:1993   3rd Qu.:70.85  
   Australia  :  12                  Max.   :2007   Max.   :82.60  
   (Other)    :1632                                                
     population        gdp_per_capita    
   Min.   :6.001e+04   Min.   :   241.2  
   1st Qu.:2.794e+06   1st Qu.:  1202.1  
   Median :7.024e+06   Median :  3531.8  
   Mean   :2.960e+07   Mean   :  7215.3  
   3rd Qu.:1.959e+07   3rd Qu.:  9325.5  
   Max.   :1.319e+09   Max.   :113523.1  
  

人口等较大数值可能以科学计数法显示。这只是打印格式,不会改变对象中的数值。

结构化扫描:skimr::skim()

skimr 不属于 tidyverse 核心包。首次使用前,可在 Console 中运行 install.packages("skimr")。课程代码使用 skimr:: 明确函数来源,无需另行运行 library(skimr)。

eda_data %>%
  skimr::skim() %>%
  skimr::focus(
    skim_variable,
    n_missing,
    complete_rate,
    numeric.mean,
    numeric.sd,
    numeric.hist
  )
Data summary
Name Piped data
Number of rows 1704
Number of columns 6
_______________________
Column type frequency:
factor 2
numeric 4
________________________
Group variables None

Variable type: factor

skim_variable n_missing complete_rate
country 0 1
continent 0 1

Variable type: numeric

skim_variable n_missing complete_rate mean sd hist
year 0 1 1979.50 17.27 ▇▅▅▅▇
life_expectancy 0 1 59.47 12.92 ▁▆▇▇▇
population 0 1 29601212.32 106157896.74 ▇▁▁▁▁
gdp_per_capita 0 1 7215.33 9857.45 ▇▁▁▁▁

阅读输出时,可先查看三类信息:n_missing 和 complete_rate 描述完整性;numeric.mean 与 numeric.sd 给出数值摘要;numeric.hist 提供粗略的分布轮廓。迷你直方图适合发现偏态或双峰线索,正式判断仍需结合后续图形和原始记录。

扫描结果需要联系变量说明

统计软件只能报告实际存储的类型和数值。年龄为 999、收入为负数等记录是否合理,需要依据问卷编码、数据字典和研究情境判断。

描述统计
5.1.2 描述变量分布

summarise() 可以围绕研究问题组合统计量。选择指标时,应先看分布形态和变量含义,再决定报告均值、分位数或计数。

常用描述指标及其使用提示
需要回答的问题 常用指标 使用提示
典型水平在哪里 均值;中位数 均值受极端值影响;偏态数据常同时查看中位数
数据有多分散 标准差;四分位距 标准差与均值配套;四分位距与中位数配套
覆盖范围如何 最小值;最大值 极差由两端记录决定,需核对异常编码
有多少信息参与计算 总行数;有效值数;缺失数 报告统计量时同时说明分母和缺失情况
公式与 R 的计算口径

样本均值记为 \(\bar{x}\):

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

R 的 sd() 计算样本标准差,分母为 \(n-1\):

\[ s = \sqrt{\frac{1}{n-1}\sum_{i=1}^{n}(x_i-\bar{x})^2} \]

中位数是排序后的中间位置;四分位距为 \(IQR = Q_3-Q_1\)。均值和标准差使用全部数值,因此更容易受到极端值影响;中位数和四分位距关注排序位置,对少量极端值较稳定。

下面同时报告总行数、有效值数和缺失数。即使当前变量没有缺失,这种写法也能明确统计量的计算分母。

eda_data %>%
  summarise(
    n_total = n(),
    n_valid = sum(!is.na(life_expectancy)),
    n_missing = sum(is.na(life_expectancy)),
    mean_life = mean(life_expectancy, na.rm = TRUE),
    median_life = median(life_expectancy, na.rm = TRUE),
    sd_life = sd(life_expectancy, na.rm = TRUE),
    iqr_life = IQR(life_expectancy, na.rm = TRUE)
  )
  # A tibble: 1 × 7
    n_total n_valid n_missing mean_life median_life sd_life iqr_life
      <int>   <int>     <int>     <dbl>       <dbl>   <dbl>    <dbl>
  1    1704    1704         0      59.5        60.7    12.9     22.6

na.rm = TRUE 只影响当前函数的计算,原数据中的 NA 仍然保留。它也可能掩盖较多缺失,因此需要与 n_valid、n_missing 或缺失比例一起报告。

新增检查:定位两端记录

摘要数字指出分布范围,原始记录用于判断这些值来自真实差异、录入错误还是特殊样本。

bind_rows(
  eda_2007 %>%
    slice_min(life_expectancy, n = 3, with_ties = TRUE) %>%
    mutate(position = "最低值"),
  eda_2007 %>%
    slice_max(life_expectancy, n = 3, with_ties = TRUE) %>%
    mutate(position = "最高值")
) %>%
  select(position, country, continent, life_expectancy)
  # A tibble: 6 × 4
    position country          continent life_expectancy
    <chr>    <fct>            <fct>               <dbl>
  1 最低值   Swaziland        Africa               39.6
  2 最低值   Mozambique       Africa               42.1
  3 最低值   Zambia           Africa               42.4
  4 最高值   Japan            Asia                 82.6
  5 最高值   Hong Kong, China Asia                 82.2
  6 最高值   Iceland          Europe               81.8

极端值不等于错误值

先回到数据来源核对记录、单位和编码,再决定保留、修正、转换或排除。任何处理都应留下判断依据,并比较处理前后的结果。

分组比较
5.1.3 比较分组统计

总体摘要可能掩盖组别差异。分组前应明确比较单位:Gapminder 的 2007 年截面以国家为观测单位,因此下面的计数表示国家数,每个国家在普通均值中权重相同。

分类变量:频数与比例

eda_2007 %>%
  count(continent, name = "n_countries") %>%
  mutate(
    country_share = n_countries / sum(n_countries),
    country_share_pct = scales::percent(country_share, accuracy = 0.1)
  ) %>%
  select(continent, n_countries, country_share_pct)
  # A tibble: 5 × 3
    continent n_countries country_share_pct
    <fct>           <int> <chr>            
  1 Africa             52 36.6%            
  2 Americas           25 17.6%            
  3 Asia               33 23.2%            
  4 Europe             30 21.1%            
  5 Oceania             2 1.4%

频数回答“每组有多少个国家”,比例回答“每组国家数占全部国家数的多少”。这里的比例不表示人口占比。

连续变量:按组描述分布

eda_2007 %>%
  group_by(continent) %>%
  summarise(
    n_countries = n(),
    mean_life = mean(life_expectancy, na.rm = TRUE),
    sd_life = sd(life_expectancy, na.rm = TRUE),
    median_life = median(life_expectancy, na.rm = TRUE),
    iqr_life = IQR(life_expectancy, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(mean_life))
  # A tibble: 5 × 6
    continent n_countries mean_life sd_life median_life iqr_life
    <fct>           <int>     <dbl>   <dbl>       <dbl>    <dbl>
  1 Oceania             2      80.7   0.729        80.7    0.516
  2 Europe             30      77.6   2.98         78.6    4.78 
  3 Americas           25      73.6   4.44         72.9    4.63 
  4 Asia               33      70.7   7.96         72.4   10.2  
  5 Africa             52      54.8   9.63         52.9   11.6

这张表是分组描述性统计表。2007 年 Oceania 只有 2 个国家,均值与标准差都应结合该组样本量阅读;组间描述差异不能直接替代统计检验或因果分析。

分析单位与权重会改变问题

国家等权均值回答“亚洲国家的人均 GDP 平均是多少”;人口加权均值更接近“亚洲居民所处国家的人均 GDP 平均是多少”。两种口径各自对应不同的研究问题。

eda_2007 %>%
  filter(continent == "Asia") %>%
  summarise(
    n_countries = n(),
    country_weighted_mean = mean(gdp_per_capita, na.rm = TRUE),
    population_weighted_mean = weighted.mean(
      gdp_per_capita,
      w = population,
      na.rm = TRUE
    )
  )
  # A tibble: 1 × 3
    n_countries country_weighted_mean population_weighted_mean
          <int>                 <dbl>                    <dbl>
  1          33                12473.                    5432.

当前数据中,国家等权均值约为 12,473 美元,人口加权均值约为 5,432 美元。差异来自权重定义,不能用“哪个数值更准确”来判断;报告中应写明观测单位和加权方式。

从分组摘要到 Table 1

正式的基线特征表 (Table 1) 往往同时包含多种变量。近似对称的连续变量可报告 Mean (SD),明显偏态的连续变量可报告 Median (IQR),分类变量通常报告 Count (%)。还需交代每个变量的缺失、分母、单位和加权方式。上面的单变量分组结果只是其中一个组成部分。

进阶:面板数据为什么要保留时间层

同一国家在不同年份会重复出现。直接对全部国家年度记录求均值,会把国家差异和时间变化混在一起。可以先选择一个截面,也可以将年份纳入分组。

eda_data %>%
  filter(year %in% c(1952, 2007)) %>%
  group_by(continent, year) %>%
  summarise(
    mean_gdp_per_capita = mean(gdp_per_capita, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  pivot_wider(
    names_from = year,
    values_from = mean_gdp_per_capita,
    names_prefix = "year_"
  )
  # A tibble: 5 × 3
    continent year_1952 year_2007
    <fct>         <dbl>     <dbl>
  1 Africa        1253.     3089.
  2 Americas      4079.    11003.
  3 Asia          5195.    12473.
  4 Europe        5661.    25054.
  5 Oceania      10298.    29810.

这张表描述两个时间点的组内均值。它没有控制其他因素,也不能解释变化的原因。

关系检查
5.1.4 检查变量关系

相关系数概括两个变量共同变化的方向与程度。系数的含义取决于关系形态、变量尺度、极端值和参与计算的观测数;因果结论还需要研究设计与识别假设。

Pearson 与 Spearman 相关系数的使用场景
方法 主要描述 检查重点
Pearson’s \(r\) 连续变量之间的线性关系 散点关系是否近似线性;极端值是否主导结果
Spearman’s \(\rho\) 排名之间的单调关系 变量可否按等级解释;关系是否持续同向或反向

分布偏态本身不足以决定使用哪种系数。若关系近似线性且原始数值差异有实际含义,可考虑 Pearson;等级变量、单调关系或极端值明显影响数值距离时,可同时查看 Spearman。

相关系数的公式与强弱标签

Pearson 相关系数的常见表达式为:

\[ r = \frac{\sum_{i=1}^{n}(x_i-\bar{x})(y_i-\bar{y})} {\sqrt{\sum_{i=1}^{n}(x_i-\bar{x})^2}\sqrt{\sum_{i=1}^{n}(y_i-\bar{y})^2}} \]

Spearman 相关先将观测转换为秩,再计算秩之间的相关。若没有并列秩,可写为:

\[ \rho = 1-\frac{6\sum d_i^2}{n(n^2-1)} \]

相关系数取值在 -1 到 1 之间。正负号表示方向,绝对值越大表示相应类型的关联越强。固定的 0.3、0.5 或 0.7 阈值没有跨领域通用的解释;报告时应给出系数、样本量、不确定性和研究背景。

下面先建立成对完整的数据,再计算两类系数和检验结果。对 gdp_per_capita 取常用对数,可减小右偏和数量级差异,并使散点关系更接近线性。

relationship_data <- eda_2007 %>%
  transmute(
    life_expectancy,
    log_gdp_per_capita = log10(gdp_per_capita)
  ) %>%
  drop_na()

relationship_data %>%
  summarise(
    n_complete = n(),
    pearson_r = cor(
      log_gdp_per_capita,
      life_expectancy,
      method = "pearson"
    ),
    spearman_rho = cor(
      log_gdp_per_capita,
      life_expectancy,
      method = "spearman"
    )
  )
  # A tibble: 1 × 3
    n_complete pearson_r spearman_rho
         <int>     <dbl>        <dbl>
  1        142     0.809        0.857
cor.test(
  ~ log_gdp_per_capita + life_expectancy,
  data = relationship_data,
  method = "pearson"
)
  
    Pearson's product-moment correlation
  
  data:  log_gdp_per_capita and life_expectancy
  t = 16.283, df = 140, p-value < 2.2e-16
  alternative hypothesis: true correlation is not equal to 0
  95 percent confidence interval:
   0.7433069 0.8592085
  sample estimates:
        cor 
  0.8089803
cor.test(
  ~ log_gdp_per_capita + life_expectancy,
  data = relationship_data,
  method = "spearman",
  exact = FALSE
)
  
    Spearman's rank correlation rho
  
  data:  log_gdp_per_capita and life_expectancy
  S = 68434, p-value < 2.2e-16
  alternative hypothesis: true rho is not equal to 0
  sample estimates:
        rho 
  0.8565899

2007 年截面有 142 个成对完整国家。log_gdp_per_capita 与 life_expectancy 的 Pearson 相关系数为 0.809,95% 置信区间为 [0.743, 0.859];Spearman 等级相关系数为 0.857。两项检验均为 p < 0.001,显示正向关联;这些结果不说明人均 GDP 对预期寿命的因果效应。

研究报告可写为:

在 2007 年 Gapminder 截面数据中(n = 142),人均 GDP 的常用对数与预期寿命呈正相关(Pearson’s r = 0.809,95% CI [0.743, 0.859],p < 0.001;Spearman’s \(\rho\) = 0.857,p < 0.001)。

解释相关结果时检查三件事

  1. 关系形态:Pearson 接近 0 仍可能存在曲线关系。
  2. 样本量与不确定性:很小的 p 值不代表关联很强,应同时报告系数和区间。
  3. 共同原因与重复观测:第三变量、抽样结构或同一对象的多次记录都可能影响结果。

多变量初筛:相关矩阵

相关矩阵便于同时查看多个数值变量。下面先保留四个变量都完整的国家,使矩阵中每个系数使用相同的 142 条记录。

correlation_data <- eda_2007 %>%
  transmute(
    life = life_expectancy,
    population,
    gdp_pc = gdp_per_capita,
    log_gdp_pc = log10(gdp_per_capita)
  ) %>%
  drop_na()

nrow(correlation_data)
  [1] 142
correlation_matrix <- cor(correlation_data, method = "pearson")
round(correlation_matrix, 3)
              life population gdp_pc log_gdp_pc
  life       1.000      0.048  0.679      0.809
  population 0.048      1.000 -0.056     -0.024
  gdp_pc     0.679     -0.056  1.000      0.875
  log_gdp_pc 0.809     -0.024  0.875      1.000
# corrplot 首次使用前需在 Console 中安装
corrplot::corrplot(
  correlation_matrix,
  method = "number",
  type = "upper"
)
上三角相关矩阵以数字显示预期寿命、人口、人均 GDP 和对数人均 GDP 两两之间的 Pearson 相关系数

Gapminder 2007 年四个数值变量的 Pearson 相关矩阵

相关矩阵适合初步发现高度相关的变量组合。它不能替代建模阶段的共线性诊断,也不能说明变量关系在不同组别或时间中保持一致。

何时可以写入论文

样本构成、缺失情况、主要变量的描述统计和关键分组比较,常放在论文正文或附录中,用来说明分析样本及变量特征。报告应保留与研究问题有关的结果,无需逐项展示全部探索过程。由 EDA 新产生的分析应标明为探索性分析。

延伸阅读与函数文档

6.0 章节练习与测评

6.0 章节练习与测评

本节包含三个环节。任务 1 提供可直接运行的模板,需要复制、修改并 Knit;任务 2 以小组为单位制作一份 R Markdown 报告,并在课堂展示本地 HTML;任务 3 通过选择题检查本章知识点。

01跟随示例复制、修改并 Knit
02小组实践与展示共同制作本地 HTML
03知识点自测选择答案并查看解析

任务 1:跟随示例运行

某社区研究在正式调查前进行了小规模试访,研究人员希望检查不同访谈方式的执行时长和数据记录质量。教学模拟数据中,每行应对应一名受访者的一次完整访谈;interview_mode 记录访谈方式,interview_minutes_text 记录访谈时长。原始导出包含多余空格、一条因重复导出产生的完全重复记录、“未记录”形式的缺失编码和带单位的数值文本。

建议文件名:chapter_2_follow_along.Rmd。

Step 1 先说明记录含义
在报告正文中写明:一行表示一名受访者的一次完整访谈,访谈时长的单位为分钟。随后运行 glimpse(),确认三列在清理前都以文本形式保存。

Step 2 复制并运行完整流程
下面的代码会先定位重复编号,再统一文本、转换缺失编码、提取数值并生成分组摘要。代码应按原顺序运行,暂时不要跳过中间检查。

练习代码:从试访记录到访谈方式摘要
library(tidyverse)

# 教学模拟数据:一行原本应对应一名受访者的一次完整访谈
interview_raw <- tribble(
  ~respondent_id, ~interview_mode, ~interview_minutes_text,
  "R01", "入户访谈",    "45 分钟",
  "R02", " 社区访谈点", "60分钟",
  "R03", "入户访谈",    "未记录",
  "R04", "线上访谈 ",   "30分钟",
  "R05", "线上访谈",    "35 分钟",
  "R06", "线上访谈",    "120分钟",
  "R06", "线上访谈",    "120分钟",
  "R07", "社区访谈点",  "20分钟",
  "R08", "社区访谈点 ", "25 分钟",
  "R09", "社区访谈点",  "未记录",
  "R10", "入户访谈",    "50分钟"
)

# 1. 检查变量类型和原始记录规模
glimpse(interview_raw)

# 2. 查找出现多次的受访者编号
duplicate_check <- interview_raw %>%
  count(respondent_id, name = "n_records") %>%
  filter(n_records > 1)

duplicate_check

# 3. 统一文本、转换缺失编码并提取分钟数
interview_clean <- interview_raw %>%
  mutate(
    interview_mode = str_squish(interview_mode),
    interview_minutes_text = na_if(interview_minutes_text, "未记录"),
    interview_minutes = parse_number(interview_minutes_text)
  ) %>%
  distinct()

# 4. 按访谈方式报告样本规模、缺失情况和描述统计
mode_summary <- interview_clean %>%
  group_by(interview_mode) %>%
  summarise(
    n_total = n(),
    n_valid = sum(!is.na(interview_minutes)),
    n_missing = sum(is.na(interview_minutes)),
    mean_minutes = mean(interview_minutes, na.rm = TRUE),
    median_minutes = median(interview_minutes, na.rm = TRUE),
    sd_minutes = sd(interview_minutes, na.rm = TRUE),
    iqr_minutes = IQR(interview_minutes, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(
    across(
      c(mean_minutes, median_minutes, sd_minutes, iqr_minutes),
      function(x) round(x, 1)
    )
  ) %>%
  arrange(desc(median_minutes))

knitr::kable(
  mode_summary,
  col.names = c(
    "访谈方式", "总记录数", "有效记录数", "缺失数",
    "均值", "中位数", "标准差", "四分位距"
  ),
  caption = "不同访谈方式的访谈时长描述统计"
)
## Rows: 11
## Columns: 3
## $ respondent_id          <chr> "R01", "R02", "R03", "R04", "R05", "R06", "R06"…
## $ interview_mode         <chr> "入户访谈", " 社区访谈点", "入户访谈", "线上访谈 ", "线上访谈", "线上访…
## $ interview_minutes_text <chr> "45 分钟", "60分钟", "未记录", "30分钟", "35 分钟", "120分钟…
## # A tibble: 1 × 2
##   respondent_id n_records
##   <chr>             <int>
## 1 R06                   2
不同访谈方式的访谈时长描述统计
访谈方式 总记录数 有效记录数 缺失数 均值 中位数 标准差 四分位距
入户访谈 3 2 1 47.5 47.5 3.5 2.5
线上访谈 3 3 0 61.7 35.0 50.6 45.0
社区访谈点 4 3 1 35.0 25.0 21.8 20.0

Step 3 核对处理结果
原始数据应有 11 行,duplicate_check 应指出 R06 出现两次,清理后应保留 10 行。每种访谈方式都应满足 n_total = n_valid + n_missing。

Step 4 解释均值与中位数
线上访谈的平均时长约为 61.7 分钟,中位数为 35 分钟。报告需要指出 120 分钟这条记录如何影响均值,同时说明这是一份规模很小的试访模拟数据,不能据此判断访谈方式导致了时长差异。

任务 1 完成标志

  • 已说明一行记录、访谈方式和时间单位的含义。
  • 已定位重复编号,并确认删除的是完全相同的重复记录。
  • 已将“未记录”转换为真正的缺失值,将带单位文本转换为数值。
  • 汇总表同时报告总记录数、有效记录数、缺失数、中心位置和离散程度。
  • 结果文字描述了数据中观察到的差异及其解释限制。
  • chapter_2_follow_along.Rmd 可以从空会话 Knit 为 HTML。

任务 2:小组实践与展示

在 chapter_2_group_report.Rmd 中使用 gapminder,完成一份可以在浏览器中打开的本地 HTML。正文和代码需要由小组自行组织,本任务不提供可直接拼接的起步代码。

研究问题

1952 年至 2007 年,各大洲国家预期寿命的分布发生了怎样的描述性变化?

Step 1 界定数据与分析单位
说明数据的时间范围和变量含义,并写明一行表示“一个国家在一个年份的记录”。将 lifeExp 重命名为符合本章规范的 life_exp,新建对象、代码块和文件均使用 snake_case。

Step 2 筛选并检查数据质量
保留 1952 年和 2007 年,报告筛选后的行数。检查 country + year 是否能唯一标识记录,并核对 life_exp 的缺失数量。发现重复或缺失时,应先记录检查结果,再决定后续处理。

Step 3 生成分组摘要
按大洲和年份计算国家数、有效记录数、预期寿命均值、中位数和四分位距。将结果整理成一张结构清楚的 HTML 表格,使两年之间的比较容易辨认。处理流程至少应包含筛选、质量检查、分组汇总和排序或重排四类操作。

Step 4 解释结果并生成网页
正文说明一项时间变化、一项洲际差异和一项解释限制。直接按国家记录计算的大洲均值对每个国家赋予相同权重,不能解释为该洲所有居民的平均预期寿命,也不能据此判断大洲差异的原因。完成后重启 R,再从头 Knit 出本地 HTML。

卡住时查看:函数线索与检查顺序
  • 数据检查可从 glimpse() 开始,再根据任务选择 filter()、count() 和缺失值判断。
  • 分组摘要需要先确定分组变量,再考虑 group_by()、summarise() 与 arrange()。
  • 五个大洲与两个年份组合后,分组摘要通常有 10 行。若行数不同,应回查筛选条件、分组变量和 key。
  • 若希望把 1952 年和 2007 年放在相邻列中,可回看 pivot_wider();转换前先确认每个“大洲 × 年份 × 指标”位置只有一个汇总值。
  • gapminder 中大洋洲只包含两个国家。解释离散程度时应同时查看组内国家数。

展示要点

  • 研究问题、一行记录的含义及保留的两个年份。
  • 数据质量检查结果和分组汇总表的组织方式。
  • 一项主要发现,以及均值、中位数或四分位距提供了什么信息。
  • 国家层面、等权汇总和描述性比较带来的一项解释限制。

展示前检查

任务 3:知识点自测

以下六道题围绕数据类型、整洁结构、管道、表连接、缺失值和 EDA 解释设置。选择答案后点击“检查答案”,页面会显示对应解析。

01interview_minutes_text 的取值为“45 分钟”“60 分钟”和“未记录”,glimpse() 显示该列为 <chr>。确认单位为分钟后,哪种处理最合适?

02一张表包含 city、visits_2020 和 visits_2021。若“年份”是分析变量,每行需要表示一条城市年度观测,应怎样理解这张表?

03运行 clean_data <- raw_data %>% filter(year == 2007) 后,哪项表述正确?

04右表中的同一个 key 出现多次时,left_join() 最可能产生什么结果?

05某组共有 40 条记录,其中 32 条 score 有效。使用 mean(score, na.rm = TRUE) 后,哪种报告最准确?

06使用 2007 年国家记录计算人均 GDP 与预期寿命的相关系数,得到较强正相关。哪项解释合适?