章节概述
本章讨论社会科学数据进入分析前需要完成的基本工作。第一部分区分 R
对象的类型、类与结构,并用变量、观测和取值解释整洁数据。随后以
tidyverse
为主要工具,练习选择行列、重命名变量、构造变量、分组汇总、连接表格和长宽转换;每项操作都配有输入、输出与结果检查。
数据处理还包括质量诊断。缺失值、重复记录、文本格式和列名需要依据数据字典与研究设计处理,不能只以代码能否运行为标准。最后一部分使用数据概览、描述统计、分组比较和相关分析检查分布、异常记录与变量关系,并区分描述性结果、探索性线索与因果解释。
R 中的每个对象都有类型和结构。类型说明值如何存储,结构说明这些值如何组合。读懂这两层信息,才能判断一段代码为什么能够计算,或为什么会报错。
R 把数值、文本和逻辑值保存在对象中。数据类型(data
type)描述对象在内存中的基本存储方式;对象类(class)则告诉 R
应采用哪一组方法处理该对象。typeof() 和
class() 因此可能返回不同结果。
| 常用类型 | 适合保存的值 | 示例 | 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
中,可从上到下逐行运行并比较输出。
## [1] "numeric"
## [1] "double"
## [1] "integer"
## [1] "integer"
引号中的 "13" 是字符值,不能直接参与加法。Excel 或 CSV
的某一列混入单位、空格或文字时,也可能被导入为字符型。
## [1] "character"
此时运行 response + 2 会报错,因为加法要求数值输入。确认
"13" 确实表示数字后,再创建转换后的新对象。
## [1] 15
先检查原始值,再进行转换。直接覆盖原对象可能掩盖录入错误或特殊编码。
NA 与缺失值检查
数据结构(data structure)描述多个值怎样组合成一个对象。选择结构时主要看三个问题:对象有几维、各元素能否使用不同类型,以及后续需要按位置还是按名称提取。
| 对象 | 维度 | 类型规则 | 常用提取 |
|---|---|---|---|
| 向量(vector) | 一维 | 元素使用同一种原子类型 | [ ] |
| 列表(list) | 一维 | 成员可以使用不同类型和结构 | [[ ]] / $ |
| 矩阵(matrix) | 二维 | 单元格使用同一种原子类型 | [行, 列] |
| 因子(factor) | 一维 | 分类标签由 levels 保存 |
[ ] / levels() |
数据框(data frame) /
tibble |
二维 | 各列等长;列与列的类型可以不同 | [[ ]] / $ / [行, 列] |
常用创建函数包括
c()、list()、matrix()、factor()、data.frame()
和 tibble::tibble()。原子向量只能使用一种类型,例如
c(13, "未回答") 会形成字符向量。[ ]
可以按位置、名称或逻辑条件保留一个或多个元素;[[ ]]
从列表或数据框中取出一个成员;$ 也可按名称提取,例如
student_data$city,但名称必须直接写在代码中。
切换对象,比较相同检查函数在不同结构上的返回结果。
score_vec <- c(
72, 88, 91
)
score_vec[2]
typeof()class()88向量中的三个元素都按双精度数值存储。
社会科学数据常以行和列组成表格。在 R 中,常见表格对象是
data.frame 和 tibble。tibble 继承
data.frame,主要改进打印方式和部分子集操作的行为;两者都由等长列组成。
gapminder 包提供同名的 gapminder
数据对象,整理了 Gapminder 项目的历史国家数据。该表包含 1,704 条记录和 6
个变量,涉及 142 个国家、5 个大洲和 12 个年份。
lifeExp 为出生时预期寿命(年),pop 为人口,gdpPercap 为人均 GDP其余两列是国家 country 和大洲
continent。字段名来自数据包原表,本章沿用
lifeExp 和 gdpPercap 等原名。
library(gapminder) 在当前 R 会话中加载包;随后输入
gapminder,即可预览同名数据对象。代码块关闭包加载消息和
warning,使网页只保留数据输出。在 Console
中运行时,仍可能看到与本机包版本有关的提示。
# 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
<fct> <int> <dbl>
ℹ 1,694 more rows
<int> 与 <dbl>
描述存储方式。变量是计数、年份还是连续测量值,仍由数据含义决定。
tibble 在 R Console
中会根据可用宽度显示适量的行和列,因此打印行数可能随设置变化。Knit
写入网页后,打印结果保留生成时的排版。需要明确控制预览范围时,可使用
head() 或 print(n = ...)。
Knit 会在独立的 R 会话中按顺序运行代码,并把结果写入
HTML。knitr::kable() 可将数据框转为适合网页阅读的表格。
| 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 对象中的原始值。
| 表格要素 | 含义 | 在 gapminder 中的对应内容 |
|---|---|---|
| 数据集(dataset) | 围绕同一主题组织的整张表 | 整个 gapminder 表 |
| 变量(variable) | 描述某项属性的一列 | country、year、lifeExp
等 |
| 观测(observation) | 表中的一行记录 | 某国某年的一组指标 |
| 观测单位(unit of observation) | 一行对应的对象与情境 | 国家 × 年份 |
country 和 year 共同定位一条记录。只看
country 会遗漏时间差异,只看 year
会混合多个国家。确认观测单位后,才能判断重复记录、缺失值和汇总结果是否合理。
初次读取一张表时,可依次检查规模、列名、对象类、列结构、首尾记录和缺失情况。下面的代码都使用已经加载的
gapminder 对象,可以直接运行。
dim(gapminder) # 行数与列数
names(gapminder) # 列名
class(gapminder) # 对象类
str(gapminder) # 各列结构与部分取值
head(gapminder) # 前 6 行
tail(gapminder) # 后 6 行
anyNA(gapminder) # 是否存在 NAstr()
把对象类、维度和逐列信息集中显示,适合第一次检查数据。
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 [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。这项检查只能回答“是否出现缺失值”,不能说明样本是否完整或数据是否具有代表性。
整洁数据(tidy data)是一套组织数据的约定:让变量、观测和取值在表格中各有明确的位置,便于理解、分析与复核。
调查问卷、业务记录和统计报表常按不同方式保存。面对同类分析任务,研究者因而需要反复辨认和调整数据结构。Hadley Wickham 在 2014 年发表的 Tidy Data 中系统阐述了整洁数据框架,目的是通过一致的数据结构减少重复整理,让数据处理、建模和可视化工具更容易衔接(Wickham,2014)。
整洁性取决于数据的含义与表格结构是否对应。理解规则之前,需要先区分三个概念:变量(variable)记录某一种属性,观测(observation)记录研究对象在指定条件下的情况,取值(value)则是某条观测在某个变量上的具体内容。
例如,研究两座城市的公共图书馆使用情况,可以把“一座城市在一个年份的记录”定义为一条观测。下表包含城市、年份、到馆人次三个变量,共有四条观测;到馆人次以万人次计,数值均为教学模拟数据。
| 城市 | 年份 | 到馆人次(万人次) |
|---|---|---|
| A 市 | 2019 | 120 |
| A 市 | 2020 | 110 |
| B 市 | 2019 | 95 |
| B 市 | 2020 | 90 |
每个变量各占一列
变量表示记录的某一种属性。“到馆人次”这一列只记录到馆人次,各行使用相同的计量单位;城市和年份分别放在另外两列。列中的数值可以不同,但含义应一致。
每条观测各占一行
本例的一条观测是一座城市在一个年份的记录。最后一行把“B 市”“2020 年”和“90 万人次”放在一起。B 市在 2019 年的记录属于另一条观测,因此城市名称可以在不同的行中重复。
每个单元格保存一个值
取值是某条观测在某个变量上的具体内容。右下角的“90”表示 B 市 2020 年的到馆人次。它的含义需要结合所在行、列名和计量单位一起理解;数字和城市名称都可以是取值。
这三条表内规则采用 R for Data Science 的表述。Wickham(2014)还强调,每类观测单位各成一张表。例如,图书馆基本信息可以每馆一行,城市年度统计则每个城市每年一行;两类记录分别保存,并注明城市等关联信息。
整洁数据中变量、观测与单元格的对应关系
先明确变量的含义和观测单位,再判断排法。 行列数量、表格外观或单元格颜色,都不足以单独判断数据是否整洁。
下面仍是两座城市的四个年度数值,每行把同一城市的两年数据并列展示。
| 城市 | 2019 | 2020 |
|---|---|---|
| A 市 | 120 | 110 |
| B 市 | 95 | 90 |
在“每个城市每年一条观测”的约定下,年份本身是一个变量。这里把年份的取值写进列名,一行便包含了两条年度观测。与前面的四行表相比,数值没有改变,变量和观测所处的位置改变了。
宽表便于并排阅读,是否整洁仍取决于各列的含义。若一人只测量一次,身高和体重是两个不同的变量,适合各占一列;它们的单位分别为厘米和千克。将各年数值排在多列,与把身高、体重排在两列,需要分别判断。
一致的数据结构使分析任务更容易对应到明确的变量和观测。在城市年度案例中,这种对应关系体现在:
整洁结构与数据质量需要分别检查
表格符合整洁规则,仍可能存在缺失值、重复记录或错误数值。例如,同一个城市同一年出现两条重复记录,或到馆人次被误填为负数,行列外观依然可以十分规整。整洁规则说明如何组织数据,数值是否可信还需要结合来源、计量单位与调查过程判断。
可以同时提供观测单位、变量含义与计量单位。例如:“每行是一座城市某一年的记录,到馆人次以万人次计。”这些说明可作为核对 AI 解释的依据。整洁结构不能保证 AI 正确理解变量,更不能替代对原始记录的核验;涉及个人信息时,应先按数据使用要求处理,避免上传可识别个人的记录。
tidyverse 是一组为数据分析设计的 R 包。 这些包采用相近的写法,分别承担数据读取、整理、计算和绘图等任务。对于表格数据,常见操作包括选择行列、计算新变量、分组汇总和关联多张表。
tidyverse 也指统一安装和加载这组核心包的入口包。运行
library(tidyverse) 后,常用函数便可在当前 R
会话中调用。本节重点使用 dplyr 和 tidyr:
| 包 | 在数据分析中的作用 | 本节中的例子 |
|---|---|---|
| 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。只运行管道并打印结果,不会改写原对象。
后面的示例也遵循这一规则。
从数据任务选择函数
下面 12 项按行列操作、计算汇总和表格整理排列。每项先给出基础用法;参数交互和折叠区用于观察差异或进一步练习。
读图约定:
示意图中的中文字段用于辨认变量含义,正文代码块是可复制、可运行的课程版本。原始列名若含空格、括号等符号,需要使用反引号包围;课程代码继续采用
snake_case。
glimpse()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() 按变量列出名称、类型和部分取值,原表保持不变
select()select() 选择数据的列。按名称列出需要的变量,结果就按给定顺序排列;在列名前加
- 可以排除该列。普通未分组数据的行数保持不变。
# 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()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()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
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" 需要引号。
在同一张成绩表上调整年份、分数和条件组合,观察筛选后的行数。
| id | group | year | score |
|---|---|---|---|
| 1 | A | 2020 | 55 |
| 2 | A | 2021 | 70 |
| 3 | B | 2020 | 70 |
| 4 | B | 2021 | 85 |
| 5 | C | 2020 | 85 |
| 6 | C | 2021 | 95 |
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)| id | group | year | score |
|---|---|---|---|
| 3 | B | 2020 | 70 |
| 5 | C | 2020 | 85 |
年份和分数两个条件都为 TRUE 的行才被保留。 结果:2 行 × 4 列。
预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。
条件为 NA 的行也不会保留。
filter(!is.na(life_exp))
明确删除寿命缺失的记录。若希望保留“寿命至少 75
或寿命缺失”的记录,条件应写为
life_exp >= 75 | is.na(life_exp)。缺失值的处理决定需要结合分析目的。
str_detect()
检查单元格的文本是否符合模式。例如:
# A tibble: 2 × 2
country year
<fct> <int>
1 United Kingdom 2007
2 United States 2007
^United 表示以 United
开头。这属于模式匹配,不会自动识别拼写错误或同义名称。选择列名的
matches() 与检查列内取值的 str_detect()
作用位置不同。
arrange()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 保持不变。
arrange() 按到馆人次降序重排记录,整行数据一起移动
本地数据框中的 NA 排在最后,使用 desc()
时也如此。字符排序受语言区域设置影响;中文姓名或地区名不宜直接解释为“按拼音顺序”。
slice_*()取出表格开头的记录、取出指标最高的记录和随机抽取记录,分别服务于预览、极值比较和抽样。slice_*()
用不同函数区分这些操作。
| 函数 | 常用参数 | 含义 |
|---|---|---|
slice_head() |
n = 3 |
按当前顺序取前三行 |
slice_max() |
order_by、n、with_ties |
按指定变量取最大值记录 |
slice_sample() |
n 或 prop、replace |
随机抽取记录 |
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 次抽取,随后可从头重跑。
| id | group | year | score |
|---|---|---|---|
| 1 | A | 2020 | 55 |
| 2 | A | 2021 | 70 |
| 3 | B | 2020 | 70 |
| 4 | B | 2021 | 85 |
| 5 | C | 2020 | 85 |
| 6 | C | 2021 | 95 |
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)| id | group | year | score |
|---|---|---|---|
| 3 | B | 2020 | 70 |
| 6 | C | 2021 | 95 |
| 2 | A | 2021 | 70 |
当前为同一种子起点后的第 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()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()
的三个主要参数依次是条件、成立时的值、不成立时的值;两种结果需要具有兼容的类型。条件缺失时,默认结果也是缺失。
mutate() 根据已有变量计算新变量,同时保留原有记录
根据分数生成分类标签,比较新增一列与更新已有列。
| id | group | year | score | status |
|---|---|---|---|---|
| 1 | A | 2020 | 55 | 未评定 |
| 2 | A | 2021 | 70 | 未评定 |
| 3 | B | 2020 | 70 | 未评定 |
| 4 | B | 2021 | 85 | 未评定 |
| 5 | C | 2020 | 85 | 未评定 |
| 6 | C | 2021 | 95 | 未评定 |
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, "达标", "待提升"
))| id | group | year | score | status | score_group |
|---|---|---|---|---|---|
| 1 | A | 2020 | 55 | 未评定 | 待提升 |
| 2 | A | 2021 | 70 | 未评定 | 达标 |
| 3 | B | 2020 | 70 | 未评定 | 达标 |
| 4 | B | 2021 | 85 | 未评定 | 达标 |
| 5 | C | 2020 | 85 | 未评定 | 达标 |
| 6 | C | 2021 | 95 | 未评定 | 达标 |
分数达到 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()
可以把同一种计算应用到指定列。下面用一个函数,将两列数值分别保留一位小数:
# 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()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"
取消结果中的分组状态,避免后续操作继续按旧分组执行。
group_by() 指定城市分组,summarise() 为每组生成一行汇总结果
| 统计目的 | 函数 | 需要辨认的含义 |
|---|---|---|
| 数记录 | n() |
统计组内行数 |
| 数不同对象 | n_distinct(country) |
同一国家出现多次只计一次 |
| 算均值、中位数 | mean()、median() |
明确哪些记录参与计算 |
| 求总和 | sum(pop) |
总和仅覆盖数据集所含对象 |
选择分组变量与统计量,比较原始记录和汇总结果的行数。
| id | group | year | score |
|---|---|---|---|
| 1 | A | 2020 | 55 |
| 2 | A | 2021 | 70 |
| 3 | B | 2020 | 70 |
| 4 | B | 2021 | 85 |
| 5 | C | 2020 | 85 |
| 6 | C | 2021 | 95 |
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")| group | mean_score |
|---|---|
| A | 62.5 |
| B | 77.5 |
| C | 90 |
每行对应一个小组。同组内的记录参与同一次计算。结果已取消分组。 结果: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() 更紧凑:
# 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()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,这不代表属于某个相反类别。
left_join() 保留左表记录并按城市补充区域类型,未匹配处为 NA
两张表均记录城市年度信息。先用城市与年份共同匹配,再观察只按城市匹配的结果。
| city | year | visits |
|---|---|---|
| A | 2019 | 120 |
| A | 2020 | 110 |
| B | 2019 | 95 |
| B | 2020 | 90 |
| city | year | budget |
|---|---|---|
| A | 2019 | 30 |
| A | 2020 | 32 |
| B | 2019 | 25 |
| B | 2020 | 28 |
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"))| city | year | visits | budget |
|---|---|---|---|
| A | 2019 | 120 | 30 |
| A | 2020 | 110 | 32 |
| B | 2019 | 95 | 25 |
| B | 2020 | 90 | 28 |
城市与年份共同确定年度记录。本例一对一匹配,4 条到馆记录分别取得对应预算。 结果:4 行 × 4 列。
预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。
检查匹配关系,不能只看是否运行成功。 右表某个 key 重复,可能使左表的一条记录匹配多行,形成一对多;两边对应 key 都重复时,可能形成多对多。连接后应检查行数、关键字段及未匹配记录。
字符与因子等部分类型可以兼容,字符与数值等不兼容类型通常会报错。转换类型前还要检查前导零、空格与字段含义,避免把不同编号误当成相同值。
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()不同年份或调查批次的数据,如果每行记录的含义一致,可以使用
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
bind_rows() 将列结构一致的 2019 年与 2020 年记录按行追加
.id = "source_batch"
新增一列记录来源。某一批数据缺少的列会补为
NA;同名列的类型需要兼容。追加前应核对变量含义、计量单位和编码规则,列名相同并不保证口径一致。
bind_rows() 增加记录,left_join() 按 key
补充信息。bind_cols()
则直接按当前行位置拼列,不检查同一行是否属于同一个对象。只有两边行数、顺序与记录含义均已确认对应时,才适合按位置拼列。
separate_wider_delim()当同一列按固定规则合存多个变量时,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() 按指定分隔符把多列连接成字符:
# 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
用于展示,分析时仍可直接使用独立的姓名、性别和年龄列。
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_*()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
指定单元格填入到馆人次。
pivot_longer() 将年份列整理为名称和值两列,pivot_wider() 执行相反方向的转换
使用同一组城市到馆人次,比较长表与宽表。数值单位为万人次,均为教学模拟数据。
| city | 2019 | 2020 |
|---|---|---|
| A | 120 | 110 |
| B | 95 | 90 |
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)
)| city | year | visits |
|---|---|---|
| A | 2019 | 120 |
| A | 2020 | 110 |
| B | 2019 | 95 |
| B | 2020 | 90 |
原列名中的年份进入 year;四个到馆人次数值进入 visits。结果为 4 行,每行一条城市年度记录。 结果:4 行 × 3 列。
预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。
同一个结果位置应对应一个值。
若同一城市同一年存在多条到馆记录,pivot_wider()
无法直接决定填哪一个值,可能提示并生成列表列。应先核对观测单位和重复原因;只有确实需要汇总时,才通过
values_fn 指定计算,不能用求均值掩盖重复问题。
长表与宽表都可能适合分析。不同独立变量各占一列的宽表可以符合整洁规则;年份作为变量时,将年份取值单独列出通常更便于按年筛选和分组。转换应服务于变量含义和分析任务。
从质量诊断到可核对的清洗结果
数据清洗(data cleaning)用于识别并处理会妨碍计算或扭曲解释的数据问题。整洁数据规定变量、观测与取值如何排列表格;质量诊断进一步检查记录是否缺失、重复、误编码或命名不一致。
清洗时应保留原始文件,将处理结果写入新对象,并记录每一步的判断依据。这样才能发现误删、误填或单位转换错误。
NAR 使用 NA 表示缺失值(missing
value)。NA
说明某个取值未知或未记录,但没有说明缺失原因。它也不同于数值
0、空字符串 "" 和文本
"NA";后两者仍是普通字符,读取数据时未必会自动转换为真正的缺失值。
| 数据中的形式 | R 的理解 | 处理前需要确认的事项 |
|---|---|---|
NA |
缺失值 | 缺失如何产生,是否影响当前分析变量 |
0 |
数值零 | 零是否表示真实的“没有”或“未发生” |
"" |
长度为零的文本 | 空白是否应重编码为 NA |
"NA"、"未知" |
普通文本标签 | 数据字典是否把这些标签定义为缺失 |
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
# A tibble: 1 × 4
city year visits status
<chr> <dbl> <dbl> <chr>
1 A 市 2020 NA 开放
处理缺失值前,需要结合数据来源、变量用途和缺失机制决定方案。删除会改变分析样本;填补会加入原本没有观测到的数值;保留缺失则要求后续函数能够明确处理
NA。
缺失记录删除与均值填补会改变样本行数或数值分布
图中的 original_data
代表尚未清洗的原表。均值填补用于比较两种操作产生的差异,不代表正式分析中的默认选择。
| 处理方式 | 常用操作 | 适用条件与影响 |
|---|---|---|
| 删除记录 | drop_na() |
删除含指定缺失值的行;需检查样本构成与样本量变化 |
| 填入明确类别或数值 | replace_na() |
只有数据字典或研究设计能够支持该替代值时才使用 |
| 沿相邻记录补齐 | fill() |
适合组内本应重复记录的属性;必须先分组、排序并核对时间连续性 |
# 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。均值、中位数和单次模型填补都会改变变量分布,并低估填补带来的不确定性。
distinct()重复记录(duplicate
records)可能来自重复导入、一对多连接,也可能是同一对象的多次有效测量。判定重复之前必须明确观测单位和
key。若每行表示“一座城市在一个年份的记录”,则 city + year
才能共同标识一条城市年度观测;同一城市出现在多个年份是合理现象。
三种需要分开的情况
“重复记录数”至少有两种口径。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
# A tibble: 2 × 3
city year n
<chr> <dbl> <int>
1 A 市 2020 2
2 B 市 2020 2
整行去重与仅按城市去重产生的不同结果
图中上方的 distinct() 删除两条额外的整行副本,仍保留 4
条城市年度记录。下方仅按 city 去重后只剩 2 行,因为 key
与观测单位不一致;该分支用于说明错误选择 key 的后果。
同一张表分别按全部变量、城市与年份、仅城市判断重复。观察 key 与观测单位不一致时发生的数据丢失。
| city | year | visits |
|---|---|---|
| A 市 | 2019 | 120 |
| A 市 | 2020 | 110 |
| A 市 | 2020 | 110 |
| B 市 | 2019 | 95 |
| B 市 | 2020 | 90 |
| B 市 | 2020 | 90 |
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()| city | year | visits |
|---|---|---|
| A 市 | 2019 | 120 |
| A 市 | 2020 | 110 |
| B 市 | 2019 | 95 |
| B 市 | 2020 | 90 |
所有变量完全相同的副本被删除。A 市和 B 市的 2019、2020 年记录仍被保留。 结果:4 行 × 3 列。
预览为本页所列设置的 R 计算结果;复制完整代码可在 R 中运行。
.keep_all = TRUE
不会决定哪条记录更可信
distinct(city, year, .keep_all = TRUE) 对每个 key
保留当前排序中的第一行。若重复行的数值不同,应先追查差异来源,并按照日期、版本或质量标记写出可复核的保留规则。单纯改变排序只能改变保留结果,不能证明该记录正确。
stringr、readr多源数据常把同一类别写成不同形式,例如 "上海" 与
" 上海 ";也可能把 "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。单位和换算规则应写入变量说明并在清洗后核对。
janitorsnake_case含空格、括号和符号的列名需要用反引号引用,也容易在连续处理时写错。本课程采用
snake_case:英文小写单词之间使用下划线,例如
annual_income。这是课程和项目的统一约定,并非所有 R
项目的唯一规范。
janitor::clean_names()
可以批量规范列名。首次使用前,可在 Console 中运行
install.packages("janitor");安装完成后,Rmd 通过
包名::函数名() 直接调用函数。
| 原列名 | 清洗后列名 |
|---|---|
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()
核对结果。后续代码必须使用新的列名;列名规范化不会改变单元格中的数据。
完成清洗后的最低检查
drop_na()
的列选择规则;相关页面可继续进入 replace_na() 与
fill() 文档。distinct() 的变量选择与
.keep_all 参数。str_trim() 与
str_squish()。clean_names()
的命名规则与可选参数。重要
探索性数据分析 (Exploratory Data Analysis,EDA) 是一组用来认识数据、发现问题并形成后续分析判断的检查活动。统计学家 John W. Tukey 在 1977 年出版的 Exploratory Data Analysis 中系统阐述了这一思路:先观察数据呈现出的分布、差异和异常,再决定哪些统计方法与研究问题相匹配。
EDA 会出现在变量构造、建模和结果复核等多个阶段。一次检查可能暴露新的缺失、编码问题或组别差异;完成修正后,还需重新查看相关摘要。因此,EDA 通常是一个反复检查并记录判断的过程。
一份可复核的探索记录,至少应回答四类问题。
EDA 没有适用于所有研究的固定清单。下面的顺序适合社会科学数据的初次检查,也便于在分析日志中记录每一步的发现和处理决定。
探索与验证的边界
查看数据后产生的新解释或假设,应在报告中标明为探索性发现。需要验证的研究问题通常还要结合预先设定的假设、独立数据或合适的研究设计。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 年截面。
初次接触一份数据时,应先确认变量类型、取值范围和缺失情况。summary()
适合快速查看基础摘要;skimr::skim()
会把不同类型变量的统计量整理成结构化报告。
基础扫描:summary()
对数值变量,summary()
通常给出最小值、四分位数、中位数、均值和最大值;对分类变量,它会显示各类别的记录数。输出内容会随变量类型变化。
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
)| 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、收入为负数等记录是否合理,需要依据问卷编码、数据字典和研究情境判断。
summarise()
可以围绕研究问题组合统计量。选择指标时,应先看分布形态和变量含义,再决定报告均值、分位数或计数。
| 需要回答的问题 | 常用指标 | 使用提示 |
|---|---|---|
| 典型水平在哪里 | 均值;中位数 | 均值受极端值影响;偏态数据常同时查看中位数 |
| 数据有多分散 | 标准差;四分位距 | 标准差与均值配套;四分位距与中位数配套 |
| 覆盖范围如何 | 最小值;最大值 | 极差由两端记录决定,需核对异常编码 |
| 有多少信息参与计算 | 总行数;有效值数;缺失数 | 报告统计量时同时说明分母和缺失情况 |
样本均值记为 \(\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
极端值不等于错误值
先回到数据来源核对记录、单位和编码,再决定保留、修正、转换或排除。任何处理都应留下判断依据,并比较处理前后的结果。
总体摘要可能掩盖组别差异。分组前应明确比较单位: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.
这张表描述两个时间点的组内均值。它没有控制其他因素,也不能解释变化的原因。
相关系数概括两个变量共同变化的方向与程度。系数的含义取决于关系形态、变量尺度、极端值和参与计算的观测数;因果结论还需要研究设计与识别假设。
| 方法 | 主要描述 | 检查重点 |
|---|---|---|
| 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
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)。
解释相关结果时检查三件事
多变量初筛:相关矩阵
相关矩阵便于同时查看多个数值变量。下面先保留四个变量都完整的国家,使矩阵中每个系数使用相同的 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
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"
)Gapminder 2007 年四个数值变量的 Pearson 相关矩阵
相关矩阵适合初步发现高度相关的变量组合。它不能替代建模阶段的共线性诊断,也不能说明变量关系在不同组别或时间中保持一致。
何时可以写入论文
样本构成、缺失情况、主要变量的描述统计和关键分组比较,常放在论文正文或附录中,用来说明分析样本及变量特征。报告应保留与研究问题有关的结果,无需逐项展示全部探索过程。由 EDA 新产生的分析应标明为探索性分析。
skim()、focus() 及输出字段。summarise() 文档:核对分组汇总与 .groups
参数。cor.test() 文档:查阅 Pearson、Spearman 与 Kendall
检验参数。本节包含三个环节。任务 1 提供可直接运行的模板,需要复制、修改并 Knit;任务 2 以小组为单位制作一份 R Markdown 报告,并在课堂展示本地 HTML;任务 3 通过选择题检查本章知识点。
某社区研究在正式调查前进行了小规模试访,研究人员希望检查不同访谈方式的执行时长和数据记录质量。教学模拟数据中,每行应对应一名受访者的一次完整访谈;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
分钟这条记录如何影响均值,同时说明这是一份规模很小的试访模拟数据,不能据此判断访谈方式导致了时长差异。
chapter_2_follow_along.Rmd 可以从空会话 Knit 为
HTML。在 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()。pivot_wider();转换前先确认每个“大洲 × 年份 ×
指标”位置只有一个汇总值。gapminder
中大洋洲只包含两个国家。解释离散程度时应同时查看组内国家数。以下六道题围绕数据类型、整洁结构、管道、表连接、缺失值和 EDA 解释设置。选择答案后点击“检查答案”,页面会显示对应解析。
© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆
最后更新:2026年10月10日 构建环境:R version 4.6.1 (2026-06-24 ucrt)