章节概述

本章聚焦于“把数据变得可分析”。围绕数据类型与结构整洁数据tidyverse 的核心动词链,本章将建立一套从“看懂表格”到“规范处理”的基础能力:理解数据集、变量与观测单位的关系,掌握 select() / filter() / mutate() / group_by() + summarise() 等常用操作,并学会通过 left_join() 将多表按共同字段(key)对齐合并,从而形成可复用的数据整理流程。

在此基础上,本章进一步覆盖数据质量诊断与清洗策略,包括缺失值(Deletion / Imputation)、重复记录、文本标准化与隐性数值转换等关键环节,并引入 EDA 的统计框架:从 summary() 过渡到 skimr::skim() 的结构化概览,再到定制化描述统计与分组洞察,以支持更稳健的变量处理与后续建模决策。

本章学习内容

学习导航

本章学习内容

欢迎开始 第 2 章 的学习。请点击 下方导航卡 进入相应小节:

💡 提示:学习完一个小节后,请再次点击 屏幕右下角的章节主页按钮回到本导航页

1.0 基础数据类型与结构

1.0 数据类型与结构

1.1 R 语言中的基础数据类型

数据是 R 语言执行计算与分析的基础单元,构成后续数据处理与空间建模的核心要素。在 R 环境中,数据依据其底层属性被严格划分为不同的数据类型(data type / class)

以下为 R 语言中常见的基础数据类型概览:

数据类型(Type) 中文名称 代码示例(Example)
logical 逻辑型(真/假) TRUE / FALSE
integer 整数型 1L / -3L
numeric (或 double) 数值型(默认实数/浮点数) 3.14 / 2
complex 复数型 2 + 3i
character 字符型(文本数据) "Shanghai" / "Hello"
raw 原始字节型 as.raw(0xb0)

1.1.1 数据类型识别:class()

在 R 环境中,可通过调用 class() 函数查看目标对象的数据类型标签(class label)

代码执行环境与练习建议

结合第一章关于 R Project 的操作规范,建议在第二章对应的项目环境中创建独立的 R 脚本(R script),以便对示例代码进行运行与验证。

  1. 依次点击 RStudio 菜单栏的 File → New File → R Script
  2. 将文件保存为:chapter2_practice.R
    (建议存放于项目根目录或 scripts/ 文件夹内)
  3. 将后续的示例代码转存至该脚本中,并逐行执行代码。
class(TRUE) # 翻译:TRUE是什么类型的数据
class("Shanghai")
class(23)
class(23L)
class(23 + 1i)

小提示:23默认是numeric类型,而 23L才是integer

常见问题:隐性字符型数据陷阱

在数据分析过程中,常出现数值表面呈现为数字(如 13),但在 R 环境中被解析为字符型(character)的情况(如 "13"
(例如从 Excel 或 CSV 文件导入数据时,列属性被误识为文本数据)

数据类型的匹配错误会导致基础算术运算无法执行并触发报错。

x <- "13"       # 将字符 "13" 赋值给变量 x
class(x)        # 检验对象类型,结果为 character(字符型)
x + 2           # 触发报错:非数值型参数无法用于算术运算

处理方案:类型检验与强制转换

as.numeric(x) + 2 # 利用 as.numeric() 将字符强制转换为数值型,再执行计算

1.2 R 语言中的核心数据结构

在 R 语言环境中,数据结构(data structure)决定了数据的组织与存储逻辑。同构数据采用不同的数据结构,将直接影响后续子集选取、数值计算、统计汇总与数据可视化的执行效率,因此构成数据处理与探索性数据分析(EDA)的基础。

基础数据结构主要包含:一维的向量(vector)列表(list),二维的矩阵(matrix)数据框(data frame),以及多维的数组(array)。此外,因子(factor)专用于存储分类变量(categorical variables),在统计建模与分组运算中具备特殊属性。

建议在独立的 R 脚本中逐行执行下表所示的创建与索引代码,重点对比 [][ , ][[ ]]$ 符号在元素提取规则上的差异。执行运算后,建议结合 class()str() 函数检验目标对象的所属类与底层嵌套逻辑。

在上述结构中,向量数据框构成了多数复杂对象的基础单元与标准表格形态,后续多源城市数据的清洗与空间探索分析工作多以此为基础展开。

数据结构 构建函数与代码示例 子集索引示例
向量
vector
x <- c(1, 2, 3)
构建长度为 3 的数值向量 x
x[2]
提取第 2 个位置的元素
列表
list
lst <- list(num = 1, chr = "a", flag = TRUE)
构建列表 lst,支持异构元素并赋予键值名称
lst[[2]]
提取第 2 个位置的底层元素
lst$chr
按名称提取 chr 元素
矩阵
matrix
m <- matrix(1:6, nrow = 2)
将数值 1 至 6 按 2 行约束生成矩阵
m[2, 3]
提取第 2 行第 3 列的元素
数组
array
a <- array(1:8, dim = c(2, 2, 2))
构建维度为 2×2×2 的三维数组
a[1, 2, 2]
提取第 1 行、第 2 列、第 2 层维度的元素
因子
factor
f <- factor(c("男", "女", "女"))
将字符向量编码为定性分类变量
f[1]
提取第 1 个观测值
levels(f)
检视该因子的所有类别水平
数据框
data frame
df <- data.frame(id = 1:3, y = c(2, 5, 9))
构建包含 id 与 y 两列特征的表格数据
df[1, "y"]
提取第 1 行 y 列对应的观测值
df$y
提取 y 列对应的完整向量

核心数据结构特征辨析

  • 向量
    存储同质化数据的一维结构 (如单一数值序列或字符序列)。  
  • 列表
    具有层级特征的异质化容器,支持容纳不同类型的对象 (涵盖向量、数据框、空间对象或模型拟合结果等)。  
  • 矩阵 / 数组
    具备严密维度属性的同构数据集合;矩阵限定为二维,数组支持扩展至高维空间。  
  • 因子
    定性数据的特殊存储形态 (如表示特定属性、等级等分类信息),主要服务于统计计算与分组映射。  
  • 数据框
    主流的二维表格数据载体 (按列向量组织,允许各列独立定义数据类型),是关系型数据分析的基础结构。  

1.3 表格数据结构:数据集与变量(以 Gapminder 为例)

重点

在实证数据分析中,最常处理的数据形态为表格数据(tabular data)。该结构由二维的“行 × 列”构成,在 R 语言环境中通常被定义为 data.frame (或后续涉及的现代数据框格式 tibble

在执行具体的数据处理运算前,建立对表格底层逻辑的系统认知至关重要。解析标准数据表时,需准确界定以下三个核心结构要素:当前数据集(dataset)的宏观分析背景、每一列所代表的属性变量(variable),以及每一行所对应的独立观测值(observation)

后续的缺失值清洗、探索性数据分析与统计建模工作,本质上均是对上述基本元素的操作与重组。

准确解析表格底层结构是执行有效数据处理的先决条件

案例数据:Gapminder (国家发展与健康)

Gapminder 是一组常用的公开数据集(public dataset),广泛应用于数据分析与可视化教学。 该数据集以“国家 × 年份”为基础观测单元,记录了全球多国在不同历史时期的经济与健康指标。

数据表内包含以下核心变量(variables)

  • 经济指标:人均 GDP gdpPercap (GDP per capita)
  • 健康指标:预期寿命 lifeExp (life expectancy)
  • 人口指标:人口规模 pop (population)
  • 地理特征:所属大洲 continent (continent)

1.3.1 数据读取:读取Gapminder数据

说明:gapminder 是一个数据包

# 请根据第一章所学知识自行安装gapminder包
# 每次打开 RStudio 新会话后,需要加载包
library(gapminder)
gapminder # gapminder 数据对象本身就是一张表(data.frame/tibble)
  # 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

控制台输出解析:认识 tibble 数据结构

在控制台执行 gapminder 对象后,将返回上述输出结果

表头维度信息

# A tibble: 1,704 × 6

该行表明 gapminder 数据集当前以 tibble 格式呈现。

tibble ≈ data.frame (两者本质均为关系型表格数据)
tibble 是 R 语言中经过优化的现代数据框格式。其主要优势在于提供更友好的控制台排版视图 (如防止长数据填满屏幕、直观显示变量类型),且不改变基础的行列数据逻辑。

其中 1,704 × 6 明确标注了该表包含 1704 行 (观测值)6 列 (变量)


变量名与数据类型

country continent year lifeExp pop gdpPercap <fct> <fct> <int> <dbl> <int> <dbl>

第一行显示各列的变量名

  • country:国家
  • continent:所属大洲
  • year:年份
  • lifeExp:预期寿命
  • pop:人口数量
  • gdpPercap:人均 GDP

第二行提供对应变量的数据类型缩写
(此为 tibble 提供的辅助视图,非数据本体内容)

  • <fct>因子(factor)—— 定性的分类变量 (如国家、大洲)
  • <int>整数(integer)—— 无小数的离散数值 (如年份、人口)
  • <dbl>浮点数(double)—— 包含小数的连续数值 (如预期寿命、人均 GDP)

数据截断与预览机制

为提升阅读体验,tibble 默认仅渲染前 10 行观测值,并在尾部输出省略提示:

# ℹ 1,694 more rows

此提示表明另有 1694 行数据被折叠隐藏。该机制有效避免了大规模数据输出导致的控制台拥堵,便于快速进行数据预览

拓展测试
建议执行 as.data.frame(gapminder) 以观察传统数据框的输出效果,直观对比两者在排版上的差异。
(此函数可将 gapminder 强制转换为传统的 data.frame 结构)

R Markdown

我们可以使用上一章所学的 Markdown语言 knitr::kable().Rmd 中插入表格

knitr::kable(
  head(gapminder, 3), # head() 默认显示前6行,这里设置3就是前3行,以此类推 
  caption = "Table X. gapminder 数据集前 3 行",
  digits = 1  # digits=1 表示:数值列默认保留 1 位小数,便于阅读与对齐
)
Table X. 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

注意 R Markdown 只能在 .Rmdknit


1.3.2 变量与观测:解析“行”与“列”

在数据分析过程中,通常从“观测主体(研究对象)观测情境(时间/空间等条件)属性特征(具体指标)”三个维度来解析表格数据。

  • 变量(variable):描述观测主体特定属性或特征的维度。
    (例如:性别、年龄、城市人口、GDP、空气污染浓度等)

  • 数据集(dataset):由多个变量与记录构成的结构化集合,在 R 中多以表格形态呈现(如 data.frametibble

  • 观测(observation):表格中的单行记录。每一行需对应一个确定的观测单位(unit of observation)
    (例如:单一受访者、特定社区、特定城市的单日监测结果等)


gapminder 数据集中:

  • (columns) 对应 变量(variables)
    • country:国家(观测主体)
    • year:年份(观测时间)
    • lifeExp / pop / gdpPercap:核心指标(属性特征)


  • (rows) 对应 观测(observations)gapminder 的单行记录表示: 特定国家在特定年份的一组指标读数

因此,gapminder 的基本观测单位并非单一的“国家”或“年份”,而是:

国家 × 年份(Country-Year) 的面板组合。


1.3.3 数据集初步概览

导入新的数据集(如 data.frametibble后,通常需要进行初步的结构检验。
目的:确认数据规模、变量构成、字段类型以及整体表现形态

# 1) 看这张表有多大(多少行 × 多少列)
dim(数据名)

# 2) 看有哪些变量(列名)
names(数据名)

# 3) 看每一列是什么类型 + 数据结构概览(非常常用)
str(数据名)

# 4) 只预览前几行(避免直接打印刷屏)
head(数据名)

# 5) 只预览后几行(避免直接打印刷屏)
tail(数据名)

小提示:查找缺失值可以用anyNA()colSums(is.na())gapminder[!complete.cases(gapminder), ]


案例展示

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 ...


解析 str() 函数输出结果

str() (structure) 函数用于以紧凑的形式展示数据对象的底层结构。通过该函数可快速获取数据集的宏观特征,包括对象类型、维度规模、各变量的数据类型及前序观测值预览(便于进行数据形态的初步核对)


1)首行摘要:对象类型与维度规模

典型输出示例:

tibble [1,704 × 6] (S3: tbl_df/tbl/data.frame)

具体释义如下:

  • [1,704 × 6]:表明该数据集包含 1704 行6 列
  • tibble:指示对象当前呈现为 tibble 格式
    (底层仍归属于表格数据结构)
  • tbl_df/tbl/data.frame:标注对象的 S3 类标签(S3 class attributes),表明其在多数运算场景下继承了标准 data.frame 的操作逻辑。

2)逐列解析:变量结构特征

输出主体部分展示了各列的详细信息,例如:

$ country : Factor w/ 142 levels ... $ year : int [1:1704] 1952 1957 1962 ...

每行对应单一变量(列),主要包含以下结构化要素:

  • $ country / $ year:指示变量名称。 (此处的 $ 符号为 str() 的层级打印标记;在实际 R 语法中,$ 常被用作提取特定数据列的操作符,如 gapminder$year

  • 冒号后的类型标识:声明该列的基础数据类型

    • Factor:因子 (用于定性分类变量,包含固定的类别水平 levels)
    • int:整数 (integer,离散型数值)
    • num:数值 (numeric,支持小数的连续型数值)
  • 方括号与示例值:标注元素长度与前序观测值预览:

    • [1:1704]:表示该变量包含 1704 个元素 (与总行数匹配)
    • 1952 1957 ...:提供该列前序数值的直观展示 (非全量数据)

3)变量属性与数据类型映射(以 Gapminder 为例)

在实际数据集中,变量的现实属性与其 R 数据类型通常存在对应的映射关系:

  • country / continent:表征分类属性,在结构中常被编码为带有 levelsFactor
  • year / pop:表征计数或绝对时间节点,常表示为 int
  • lifeExp / gdpPercap:表征连续型的测度指标,常表示为 num

2.0 整洁数据

2.0 整洁数据

在实证数据分析中,原始数据通常难以直接用于计算。许多统计汇总、数据可视化与空间建模工作之所以遇到障碍,往往并非源于算法的复杂性,而是由于底层数据的组织结构不适配分析需求

为此,本节引入贯穿数据处理流程的核心概念:整洁数据(tidy data)。这是一种旨在优化数据清洗、汇总与可视化效率的标准化表格结构形态(Wickham, 2014)。

整洁数据三原则 (参考 Figure 1,Wickham, 2014)

核心原则

  1. 每一列对应一个变量
    (Each column is a variable)
  2. 每一行对应一条观测
    (Each row is an observation)
  3. 每个单元格仅包含单一数值
    (Each cell is a single value)

补充说明:在标准的表格构建中,通常还需保证“单一表格仅记录单一观测层级”

Figure 1. 整洁数据基本概念图

Figure 1. 整洁数据基本概念图


2.1 整洁数据的分析优势

整洁数据的核心价值在于:通过建立统一的数据组织规范,在分析初期解决底层结构问题。这种标准化形态使得后续的数据子集提取、分组汇总、可视化与空间建模能够在一致的语法框架下执行,从而有效降低因统计口径不一、重复记录或遗漏带来的误差风险,并显著提升分析工作流的可复用性与可核查性(Wickham, 2014)。


2.2 非标准数据的常见形态

在实证研究中,许多原始数据虽呈现二维表格形态,但其内在结构并不适配数据分析的逻辑要求。常见的问题包括:数据值被作为列名存储、多个独立变量被合并于单一字段,或不同观测层级的数据被混合存放。识别这些非标准结构,有助于在数据清洗与建模前明确结构重组的方向。

五类典型的非整洁数据结构

  • 列头包含数据值而非变量名
    (例如将具体年份 1952、1957 作为列名,而“年份”本应作为独立的变量列)

  • 单一列中混合多个独立变量
    (例如将“性别与年龄”、“城市与区县”合并拼接为一个字段)

  • 变量同时存在于行与列之中
    (行列结构交叉,导致基础统计与映射难以直接执行)

  • 不同观测层级的数据混杂于同一张表
    (例如将个体微观记录与区域宏观汇总指标并列存在,易引发重复计算)

  • 同级观测单位被分散存放于多个文件
    (例如同一观测维度的记录按年份拆分为多个独立文件,增加合并与管理成本)


2.2.1 结构辨析:从样例表识别非标准模式

结构辨析

以下展示的 4 个样例表 均存在特定类型的非整洁结构特征:

样例 1
person_id measure value
P001       height_cm 170
P001       weight_kg 62  
P002       height_cm 165
P002       weight_kg 58  

结构解析:表中的 measure 列存储了属性名称,属于“变量标签”而非独立的观测变量。若需计算身高或体重的统计量,该长格式结构需转换为包含 height_cmweight_kg 两个独立变量列的宽格式。

样例 2
group_id id   score group_mean
G1       A01      82         78
G1       A02      74         78
G2       B01      91         85

结构解析:表中混合了不同观测层级(粒度)的信息。个体层面的微观观测score与组级别的宏观汇总指标group_mean并列放置,在执行总体计算时会直接导致汇总指标被重复统计。

样例 3
city   2019 2020 2021
A市    120  110  115
B市     95   90   92  

结构解析201920202021 实际上是“年份”这一变量的具体取值。在整洁数据标准下,“年份”应被提取为独立的列变量,而表中的数值应归集为对应的数据列。

样例 4
person_id sex_age income
P001       F_23      5200
P002       M_31      6800

结构解析sex_age 字段混合了性别与年龄两类独立信息。若需分别执行按性别分组的汇总或年龄分布的统计,必须优先对该字段进行文本拆分,将其重构为两个独立变量。


概念补充:时间戳字段的结构定性

类似 2025/04/29 19:03:52 的时间戳(timestamp)通常被视为单一复合变量进行存储,符合整洁数据的规范要求。 仅在特定的分析场景下(如按日期、小时、星期进行周期性分组汇总),才需从时间戳中提取出 datehourweekday 等新列。此过程属于特征工程(feature engineering)范畴,而非底层结构错误。


2.3 数据整洁度的评估框架

评估准则

在数据处理实践中,判断数据表是否符合整洁数据标准,其核心不在于数据体量,而在于底层结构是否具备逻辑一致性与清晰的解释力。一套可执行的结构诊断框架主要涵盖三个维度:观测单位的界定(行记录的现实映射)变量属性的归位(列索引的独立性),以及单元格数据的原子化(单一单元格仅承载单一基元信息)。当上述三个维度均能被清晰界定时,后续的列拆分、数据合并或宽长表转换等重组操作,其执行路径将变得更具确定性与可控性


整洁度结构检验三步法

Step 1界定观测单位(明确单行记录的分析粒度)

  • 确认单行记录是对应单一受访者、特定地理单元,还是“空间 × 时间”的复合面板结构。

  • 若难以用规范的陈述句界定,通常意味着数据表中混杂了不同层级的信息,或关键变量被错误映射为行或列索引。

Step 2检验变量映射(确认变量是否独立成列)

  • 标准的列名应表征独立的变量维度(如 yearsexincome

  • 若列名呈现为具体的属性取值(如 20192020malefemale,则提示特定的变量维度(如年份、性别)被错误地横向展开为列结构。

Step 3审查单元格基元(核验“一格一值”原则)

  • 标准单元格应仅包含不可再分的原子化数值(单一数值、单一分类标签或标准时间戳)

  • 若出现复合拼接字符(如 F_23GDP_USD,则表明多个独立变量被合并存储,需进行特征分离。


2.3.1 结构重组与整理路径

重组导向

在完成结构诊断后,数据整理的核心目标可归纳为:恢复数据的标准映射关系
(即变量归于列、观测归于行、单元格原子化)。不同类型的非整洁结构对应特定的重组路径:

  • 变量横向隐匿于列名 → 将列标签重组为独立变量列
    (例:列名呈现为 19521957 等年份取值;重组目标是通过宽长转换,将“年份”提取为独立的 year 变量列)

  • 多变量嵌套于单一列 → 将复合列拆分为多个独立列
    (例:存在 male_23 等拼接字段;重组目标是将其结构化拆分为 sexage 两个独立变量,以支持后续的分组建模)

  • 单变量交叉存在于行列 → 统一变量映射维度,消除结构冗余
    (例:行索引与列索引同时反映空间类别或时间维度;重组目标是消除交叉表达,使同一变量仅存在于单一的列维度)

  • 异质观测层级混杂 → 按分析粒度进行层级分离
    (例:同一表格交替出现微观个体记录与宏观总体均值;重组目标是将明细数据与汇总数据拆分为不同表格,确保单表的观测单位绝对一致)

  • 同构数据分散存放 → 沿纵向或横向维度执行数据拼接
    (例:同结构的数据按年份拆分为多个 csv 文件;重组目标是合并为单一的长格式表格,并生成特定的来源标识列)


学习建议:逻辑主导与工具辅助

本节侧重于培养数据结构诊断思维,而非单纯强调具体函数的机械记忆。代码语法与工具细节可通过技术文档或人工智能辅助快速补充;但在实证分析中,唯有底层逻辑清晰,才能明确数据重组的下一步操作方向、内在动机,并精准运用检索工具获取解决方案。

简而言之,本教程倡导:在实施代码干预前,优先理清数据的底层结构与重组逻辑。逻辑框架的完备性,优先于代码编写的熟练度前者决定了分析的上限,后者仅为具体执行的工具手段

3.0 Tidyverse入门

3.0 使用Tidyverse包

3.1 从结构逻辑到处理方法

在本教程第一章中,已对 tidyverse 核心包集进行了基础概述。前文2.0 节也明确了实证分析所依赖的基础形态——整洁数据(即变量归于列、观测归于行、单元格原子化)。本节的核心在于将上述结构化原则转化为具体、可执行的数据操作指令。

掌握 tidyverse 语法体系的核心,在于建立基于数据结构的诊断与重构意识。在准确识别非整洁结构特征的前提下,匹配相应的处理函数(如数据子集提取、形态转换与字段拆分),可显著提升数据清洗流程的逻辑性与可解释性。


以下内容将以 gapminder 数据集为例,系统展示 tidyverse 工具箱中核心函数的输入与输出映射关系。

代码执行与验证说明

建议在独立的 R 脚本中逐行执行以下代码,并实时观察控制台(Console)或数据视图(Viewer)中的输出变化:

  • Windows: Ctrl + Enter
  • Mac: Cmd ⌘ + Enter
# 1) 加载核心包
library(tidyverse)
library(gapminder)

# 2) 准备一个测试练习数据 test_data(将 gapminder数据赋值给 test_data)
test_data <- gapminder

# 3) 确认数据已就绪
head(test_data,3)
  # A tibble: 3 × 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.

3.2 Tidyverse 核心函数

函数导航

Tidyverse 核心工具箱

本节甄选了 tidyverse 数据处理体系中最为基础且常用的 9 个核心函数
点击下方卡片,即可进入对应的功能解析与代码实践:

💡 提示:此处仅列出了 tidyverse 生态中高频使用的函数。实际数据处理中还有更多强大的工具(如 slice, rename, bind_rows 等)。

概览: glimpse()
3.2.1 数据结构概览:glimpse()

导入新数据集后,首要步骤通常为解析其底层结构,而非直接执行数据清洗或数值计算。明确数据集的维度规模、变量构成以及基础形态,将直接决定后续数据重组与分析的执行路径。

tidyverse 语法体系中,glimpse() 函数提供了一种紧凑的数据结构预览方案。该函数将表格数据进行转置显示,依次陈列变量名称、数据类型(data type)以及对应的部分前序观测值,从而支持对数据表微观结构的系统性审查。

Figure 2. glimpse() 基本概念图

Figure 2. glimpse() 基本概念图

test_data %>% glimpse() # %>% 是管道符,等于 glimpse(test_data)
  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, …
  $ lifeExp   <dbl> 28.801, 30.332, 31.997, 34.020, 36.088, 38.438, 39.854, 40.8…
  $ pop       <int> 8425333, 9240934, 10267083, 11537966, 13079460, 14880372, 12…
  $ gdpPercap <dbl> 779.4453, 820.8530, 853.1007, 836.1971, 739.9811, 786.1134, …

管道符 %>%:用顺序表达多步操作

重要

tidyverse 的工作流中,%>% 用于将左侧对象传递给右侧函数作为输入。
(例如:test_data %>% glimpse() 等价于 glimpse(test_data)

其优势在于:当需要连续执行多步处理时,可以把步骤按逻辑顺序串联成一条清晰的“处理链”,从而减少嵌套写法带来的阅读与调试负担。

简而言之,防止过度‘套娃’!

选列:select()
3.2.2 变量提取:select()

数据处理的常规首要步骤是界定所需的核心特征(即提取目标变量列,剔除冗余字段)

在处理大规模数据时,优先提取必要变量可有效降低内存消耗并提升计算效率。

tidyverse 中,select() 函数用于执行基于列的提取操作。可通过直接输入变量名保留目标列,或使用减号 - 前缀剔除特定列,从而将数据表重构为紧凑的分析子集。

Figure 3. select() 基本概念图

Figure 3. select() 基本概念图

# 例如:只保留与“国家—年份—人口-人均GDP”相关的列
test_data %>% 
  select(country, year, lifeExp, pop, gdpPercap) %>% 
  head(3)
  # A tibble: 3 × 5
    country      year lifeExp      pop gdpPercap
    <fct>       <int>   <dbl>    <int>     <dbl>
  1 Afghanistan  1952    28.8  8425333      779.
  2 Afghanistan  1957    30.3  9240934      821.
  3 Afghanistan  1962    32.0 10267083      853.

select() 基础提取语法

基础操作

基于变量名提取 - test_data %>% select(country, year, lifeExp)
(适用于目标变量较少且命名字段明确的场景)


基于列索引提取 - 连续列 (提取第 1 至第 4 列)
test_data %>% select(1:4)
- 不连续列 (提取第 1、3、5、6 列)
test_data %>% select(c(1, 3, 5, 6))
- 剔除连续列 (排除第 1 至第 2 列)
test_data %>% select(-c(1:2))
(适用于变量较多时的批量保留或剔除)


剔除特定变量 - test_data %>% select(-gdpPercap)
(适用于仅需排除少数特定列的数据降维场景)

进阶操作:基于列名模式的批量提取
(前缀 / 后缀 / 正则表达式)

进阶操作

当目标变量具有特定的命名模式特征时,可利用选择辅助函数(select helpers)进行批量提取。
(以下示例基于虚拟数据集 my_data

[Image of dplyr select helpers functions such as starts_with and contains]

  • 包含特定字符 (字符出现于任意位置)
    my_data %>% select(contains("percent"))
  • 以特定前缀起始
    my_data %>% select(starts_with("pct_"))
  • 以特定后缀结束
    my_data %>% select(ends_with("_rate"))
  • 正则表达式匹配 (regular expression)
    my_data %>% select(matches("^pct|_share$"))

若需剔除上述匹配列,在辅助函数前添加 - 即可。
(如 select(-contains("percent"))


解析 matches("^pct|_share$") (正则表达式基础)

  • matches():通过输入正则表达式对变量名进行模式匹配。
  • ^pct^ 为定位符,表示字符串起始位置,即匹配“以 pct 开头的列名”。
  • _share$$ 为定位符,表示字符串结束位置,即匹配“以 _share 结尾的列名”。
  • |:逻辑操作符,表示“或” (OR)

该语句的整体逻辑为:
提取所有以 pct 起始或以 _share 结尾的变量列


典型应用场景

  • 提取包含数字的列 (如 Q1Q30gene_1 等)
    my_data %>% select(matches("\\d"))
    \\d 代表匹配任意单一数字)

  • 提取包含特殊字符的列 (如 * 等)
    my_data %>% select(matches("\\*"))
    (正则表达式中的元字符需使用双反斜杠进行转义;此处 * 写作 \\*

  • 基于多重模式提取 (无位置限定)
    my_data %>% select(matches("rna|dna|gene|_mean$|_sd$"))
    (通过逻辑或 | 同时匹配多个关键词及特定后缀的汇总列)

  • 剔除标识列,保留指标变量 (如排除 id 与时间字段)
    my_data %>% select(-matches("^(id|ID|year|date|time)$"))
    (常用于特征标准化或统计汇总前的非数值列清洗)

  • 基于字符向量的柔性提取
    my_data %>% select(any_of(c("lifeExp", "pop", "gdpPercap", "pm25_mean", "pm25_sd")))
    any_of() 具备较高的容错率,若向量中某些列名在当前数据集中不存在,系统将自动忽略且不触发报错)

筛选:filter()
3.2.3 观测筛选:filter()

在表格数据处理中,根据特定的研究条件提取目标观测记录(即筛选数据行)是常规的基础操作。 (如仅保留特定年份、特定空间区域或满足设定阈值的样本记录)

tidyverse 语法体系中,filter() 函数专用于基于逻辑条件执行数据行的提取。通过构建特定的逻辑判断式,可精准滤除无关数据,进而构建符合研究边界的分析子集。

Figure 4. filter() 基本概念图

Figure 4. filter() 基本概念图

# 例 1:只保留 2007 年的数据并展示后 4 行
test_data %>% 
  filter(year == 2007) %>% 
  tail(4)
  # A tibble: 4 × 6
    country            continent  year lifeExp      pop gdpPercap
    <fct>              <fct>     <int>   <dbl>    <int>     <dbl>
  1 West Bank and Gaza Asia       2007    73.4  4018332     3025.
  2 Yemen, Rep.        Asia       2007    62.7 22211743     2281.
  3 Zambia             Africa     2007    42.4 11746035     1271.
  4 Zimbabwe           Africa     2007    43.5 12311143      470.
# 例 2:多条件筛选(同时满足):2007 年 + 欧洲 并展示前 4 行
test_data %>% 
  filter(year == 2007, continent == "Europe" ) %>% 
  head(3)
  # A tibble: 3 × 6
    country continent  year lifeExp      pop gdpPercap
    <fct>   <fct>     <int>   <dbl>    <int>     <dbl>
  1 Albania Europe     2007    76.4  3600523     5937.
  2 Austria Europe     2007    79.8  8199783    36126.
  3 Belgium Europe     2007    79.4 10392226    33693.
# 例 3:范围条件:1992年,人均寿命超过 75 的记录,保留 3 列变量,并展示前 3 行
test_data %>%
  filter(year == 1992, lifeExp > 75) %>%
  select(country, year, lifeExp) %>%
  head(3)
  # A tibble: 3 × 3
    country    year lifeExp
    <fct>     <int>   <dbl>
  1 Australia  1992    77.6
  2 Austria    1992    76.0
  3 Belgium    1992    76.5

filter() 条件写法速记

常用比较符号

  • ==:等于(注意:不是 =
  • !=:不等于
  • >>=<<=:大于/小于(含等号)

多个条件怎么写?

  • 同时满足(AND)
    filter(A, B)filter(A & B)
  • 满足其一(OR)
    filter(A | B)

集合匹配:%in%非常常用

  • 含义:判断“是否属于某个集合”
  • 写法
    filter(x %in% c("a", "b", "c"))
  • 示例
    test_data %>% filter(continent %in% c("Europe", "Asia"))
    (常用于只保留若干研究地区/类别)

区间筛选:一次写清“范围”

  • 写法
    filter(x >= 10, x <= 20)
    (例如保留某个阈值区间内的样本)
  • 更简洁(区间判断)
    filter(between(x, 10, 20))
    between() 读作:x 在 10 到 20 之间)

模糊匹配:只记得“包含/开头/结尾”怎么办?

当需要按字符模式筛选时,可配合 stringr::str_detect()
tidyverse 载入后可直接用)

  • 包含某关键词
    filter(str_detect(country, "land"))
    (常用于按名称关键词筛选)
  • 以某前缀开头
    filter(str_detect(country, "^United"))
    (常用于筛选统一前缀命名的记录)
  • 以某后缀结尾
    filter(str_detect(country, "stan$"))
    (常用于筛选统一后缀命名的记录)

缺失值:NA 需要显式处理

  • 仅保留非缺失
    filter(!is.na(x))
    (避免条件判断因 NA 导致记录被丢弃)
  • 仅保留缺失
    filter(is.na(x))
    (用于定位缺失值分布或后续清洗)

提醒

  • 条件中的字符串通常需要加引号:continent == "Europe"(字符比较)
  • 若需要更复杂的“模式规则”,可参考 3.2.2matches() 写法(同为正则思路)
排序:arrange()
3.2.4 观测排序:arrange()

数据处理的常规操作包含根据特定变量对观测记录进行重排,以支持极值检验、趋势观察或生成位序结果。
(如按年份升序、按人均 GDP 降序,或执行“所属大洲 → 年份”的分层多级排序)

tidyverse 工具链中,arrange() 函数专用于执行数据行的重新排列。该操作不改变基础列结构,仅调整观测记录的呈现顺序。

Figure 5. arrange() 基本概念图

Figure 5. arrange() 基本概念图

# 示例 1:按人均 GDP 升序(默认)
test_data %>% 
  arrange(gdpPercap) %>% #请注意,arrange只是改变了排序,并没有覆盖原始数据
  head(5) #如需将原始数据排列,请使用 test_data_sorted <- test_data %>% arrange(gdpPercap)
  # A tibble: 5 × 6
    country          continent  year lifeExp      pop gdpPercap
    <fct>            <fct>     <int>   <dbl>    <int>     <dbl>
  1 Congo, Dem. Rep. Africa     2002    45.0 55379852      241.
  2 Congo, Dem. Rep. Africa     2007    46.5 64606759      278.
  3 Lesotho          Africa     1952    42.1   748747      299.
  4 Guinea-Bissau    Africa     1952    32.5   580653      300.
  5 Congo, Dem. Rep. Africa     1997    42.6 47798986      312.
# 示例 2:按人均 GDP 降序
test_data %>% 
  arrange(desc(gdpPercap)) %>% 
  head(5)
  # A tibble: 5 × 6
    country continent  year lifeExp    pop gdpPercap
    <fct>   <fct>     <int>   <dbl>  <int>     <dbl>
  1 Kuwait  Asia       1957    58.0 212846   113523.
  2 Kuwait  Asia       1972    67.7 841934   109348.
  3 Kuwait  Asia       1952    55.6 160000   108382.
  4 Kuwait  Asia       1962    60.5 358266    95458.
  5 Kuwait  Asia       1967    64.6 575003    80895.

arrange() 常用写法速记

  • 升序(ascending) 默认
    arrange(x)
    (从小到大 / 从早到晚)

  • 降序(descending)
    arrange(desc(x))
    desc = descending;从大到小)

  • 多列排序
    arrange(a, b)
    (先按 a 排,再在 a 相同的组内按 b 排)

  • 混合升/降序
    arrange(a, desc(b))
    (常用于“先分组排序,再按指标做排名”)

  • 按字符排序:字符型变量默认按字典序(A → Z)
    (若需自定义顺序,可将其设为 factor 并调整 levels)

  • 常见场景
    arrange(year, desc(gdpPercap))
    (按年份组织,并在每年内查看人均 GDP 的高值记录)

构造:mutate()
3.2.5 变量派生与更新:mutate()

在数据重组与特征工程中,基于现有数据列派生新变量是常规的基础操作。
(例如:频数转比例、数值单位换算、对数变换,或生成分类标签变量)

tidyverse 语法体系中,mutate() 函数专用于执行基于列维度的新增或覆盖操作。该运算不改变基础的观测记录行数,仅在原数据表尾部追加新变量列,或通过重新赋值运算更新已有同名变量的值。

Figure 6. mutate() 基本概念图

Figure 6. mutate() 基本概念图

# 示例:基于已有变量生成新变量(创建新列)
test_data %>% 
  mutate(
    pop_million = round(pop / 1000000,1), # 百万人口:从“人数”换算为“百万”,保留1位
    gdp_total   = gdpPercap * pop,        # GDP 总量:人均 GDP × 人口
    log_gdp_pc  = log(gdpPercap)          # 人均GDP 对数变换
  ) %>% 
  select(country, year, pop_million, gdp_total, log_gdp_pc) %>% # 选择新建的列
  head(5)
  # A tibble: 5 × 5
    country      year pop_million   gdp_total log_gdp_pc
    <fct>       <int>       <dbl>       <dbl>      <dbl>
  1 Afghanistan  1952         8.4 6567086330.       6.66
  2 Afghanistan  1957         9.2 7585448670.       6.71
  3 Afghanistan  1962        10.3 8758855797.       6.75
  4 Afghanistan  1967        11.5 9648014150.       6.73
  5 Afghanistan  1972        13.1 9678553274.       6.61

mutate() 常用写法速记

  • 新增变量(新列)
    mutate(new_var = ...)

  • 一次生成多个变量
    mutate(a = ..., b = ..., c = ...)

  • 覆盖同名变量(谨慎使用)
    mutate(x = ...)
    (若 x 已存在,将被新结果覆盖;建议先用新变量名,确认无误后再覆盖)

  • 配合 if_else() 做规则标签(常用)
    例如:按年份把数据分成“2007 年”与“非 2007 年”两类:

    test_data %>% 
    mutate(year_flag = if_else(year == 2007, "Y2007", "Other")) 


    (适合生成“是/否”“高/低”“组别标签”等规则变量)

  • case_when() 做多分支规则(多类别标签更清晰)
    例如:把“国家全称”映射为简写代号,并额外给出一个“洲内优先级标签”

    test_data %>%
      mutate(
        country_code = case_when(
          country == "China"            ~ "CN",
          country == "United States"    ~ "US",
          country == "United Kingdom"   ~ "UK",
          country == "Japan"            ~ "JP",
          country == "India"            ~ "IN",
          TRUE                          ~ "Other"
        ),
        # 再举一例:用 case_when() 生成一个“分组标签”
        group_tag = case_when(
          continent == "Asia"           ~ "Asia",
          continent %in% c("Europe", "Oceania") ~ "Eur+Oce",
          TRUE                          ~ "Rest"
        )
      ) %>%
      select(country, continent, country_code, group_tag) %>%
      head(10)

    (当规则不止两类时,优先用 case_when();通常优于嵌套 if_else()

进阶:用 mutate() + countrycode 生成国家标准代号(ISO2/ISO3)

进阶操作

当数据中的国家以英文全称表示(如 ChinaUnited States时,常见需求是生成更规范、也更便于合并的国家代号(如 CN/CHNUS/USA
此时可在 mutate() 中配合 countrycode 包进行标准化映射。(使用前需先安装并加载该包)

test_data %>%
  mutate(
    iso3 = countrycode::countrycode( # 使用 countrycode里的函数
      sourcevar   = country,         # 我们数据中的 country列
      origin      = "country.name",  # 输入:国家英文全称
      destination = "iso3c"          # 输出:ISO3(如 CHN / USA / GBR)
    ),
    iso2 = countrycode::countrycode( # 使用 countrycode里的函数
      sourcevar   = country,         # 我们数据中的 country列
      origin      = "country.name",  # 输入:国家英文全称
      destination = "iso2c"          # 输出:ISO2(如 CHN / USA / GBR)
  ) 
  ) %>%
  select(country, iso3, iso2) %>%
  head(5)
  # A tibble: 5 × 3
    country     iso3  iso2 
    <fct>       <chr> <chr>
  1 Afghanistan AFG   AF   
  2 Afghanistan AFG   AF   
  3 Afghanistan AFG   AF   
  4 Afghanistan AFG   AF   
  5 Afghanistan AFG   AF

提醒:若出现 NA 映射失败通常意味着国家名称存在别名/拼写差异,可先用 filter(is.na(iso3)) 找出问题国家名,再做修正或自定义匹配

合并:left_join()
3.2.6 数据表横向关联:left_join()

常用

在多源数据分析中,单一数据表通常难以涵盖所有的分析维度:主表多用于记录核心观测指标(如人口规模、预期寿命),而附表则提供必要的属性补充(如空间区划、政策分组等)。此时需依据关联字段(key)对多张表执行横向匹配与拼接。

tidyverse 工具链中,left_join() 实现了以左表为基准的关联逻辑:在保留左表全量观测记录的前提下,将右表中匹配的变量字段追加至左表尾部。

Figure 7. left_join()基本概念图

Figure 7. left_join()基本概念图

为gapminder数据补充一个是否为OECD成员国的标签

示例

# 注意:这里只列出少数国家作为例子;未匹配到的国家会得到 NA
oecd_lookup <- tibble::tibble(
  country = c("United States", "United Kingdom", "Japan", "Germany", "France", "Canada", "Australia"),
  oecd = "OECD"
  )        # OECD成员国数据


test_data_joined <- test_data %>% left_join(oecd_lookup, by = "country")
# 快速查看合并后的结果(只展示过滤后的部分)
test_data_joined %>% 
  select(country, year, lifeExp, oecd) %>%
  filter(country == 'Germany') %>% # 看一下德国数据后是否标上了 OECD
  head()
  # A tibble: 6 × 4
    country  year lifeExp oecd 
    <chr>   <int>   <dbl> <chr>
  1 Germany  1952    67.5 OECD 
  2 Germany  1957    69.1 OECD 
  3 Germany  1962    70.3 OECD 
  4 Germany  1967    70.8 OECD 
  5 Germany  1972    71   OECD 
  6 Germany  1977    72.5 OECD

left_join() 在做什么?

核心概念

  • 必须有“共同字段”(key)
    例如使用 country 作为 key:左表 test_data 的每一行,会在右表 oecd_lookup 中查找同名的 country,并将匹配到的信息拼接到左表。

  • “左表优先”
    左表的行数与顺序不会减少;即使右表找不到匹配项,也会保留该行,只是右表新增列会出现 NA

  • by = "country" 的含义
    明确指定两张表用哪一列进行匹配。
    常见 若两表 key 列名不同,可写成:by = c("left_key" = "right_key")(左表列名 = 右表列名)


常见错误

  • 类型不一致导致匹配失败:例如左表 key 为字符,右表 key 为因子/数值。必要时先统一类型
    mutate(country = as.character(country))

  • 重复 key 引发“行数膨胀”:如果右表同一个 key 出现多行,会形成多对多匹配,结果行数可能显著增加。
    (通常需要先检查右表 key 是否唯一,并明确预期的合并关系)

其他 Join 函数
了解即可,一般不如 left_join() 常用

  • inner_join():仅保留两表都能匹配到的行(交集)

  • right_join():以右表为主(较少用;多数场景可交换左右表后用 left_join()

  • full_join():保留两表所有行(并集;无法匹配处补 NA

  • anti_join() / semi_join():用于“找差异 / 做筛选”

    • anti_join():找出左表中“在右表找不到匹配”的行
    • semi_join():仅保留左表中“能在右表匹配到”的行(不带入右表列)

补充

不通过 key 的“平行合并”
(当数据结构一致时)

有时两张表并不是“按 key 补信息”,而是行顺序与行含义完全一致(例如同一批样本、同一排序方式、同一时间序列),只需要把列横向拼接即可。此类合并不属于 join,更接近“列绑定”

# 演示用;将数据分开之后再拼回去

left_part <- test_data %>% 
  select(country, year)           # 演示用左边数据 
right_part <- test_data %>% 
  select(lifeExp, pop, gdpPercap) # 演示用右边数据

merged_cbind <- bind_cols(left_part, right_part) # 直接将左右数据按照当前排序合并

merged_cbind %>% head()
  # A tibble: 6 × 5
    country      year lifeExp      pop gdpPercap
    <fct>       <int>   <dbl>    <int>     <dbl>
  1 Afghanistan  1952    28.8  8425333      779.
  2 Afghanistan  1957    30.3  9240934      821.
  3 Afghanistan  1962    32.0 10267083      853.
  4 Afghanistan  1967    34.0 11537966      836.
  5 Afghanistan  1972    36.1 13079460      740.
  6 Afghanistan  1977    38.4 14880372      786.

补充:模糊匹配合并(fuzzy matching) 是什么?何时使用?

进阶补充

适用场景(当 key 无法“完全一致”)

当两张表的 key 存在轻微差异时,常规 left_join() 会匹配失败,例如:

  • 拼写差异 / 录入误差(typo)"Untied States" vs "United States"
  • 简称与全称不一致:"UK" vs "United Kingdom"
  • 标点/空格/大小写差异:"Cote dIvoire" vs "Côte d'Ivoire"
  • 不同数据源的命名口径不同(同义写法)

核心思想(近似匹配)
模糊匹配会根据“字符串相似度”寻找最接近的候选,从而把原本匹配不到的记录“尽可能对齐”。
结果通常会附带一个距离/相似度指标,便于人工核查匹配质量。

示例. (了解皮毛即可)

# 需要额外包:fuzzyjoin(用于模糊 join)
# install.packages("fuzzyjoin")
library(fuzzyjoin)

# 根据字符串相似度进行 left join,并输出匹配距离
result <- stringdist_left_join(
  left_tbl, right_tbl,
  by = c("country" = "country_name"),
  method = "jw",        # Jaro-Winkler(常用于名称匹配)
  max_dist = 0.15,      # 阈值越小越严格
  distance_col = "dist" # 输出距离列,便于检查
)

输出会发生什么变化?

  • 若找到近似匹配:右表信息会被补到左表,且新增 dist(匹配距离)
  • 若未达到阈值:仍保留左表行,但右表新增列为 NA(与 left_join() 行为一致)
  • 若存在多个近似候选:可能出现“一行匹配出多行”的情况(需进一步筛选或人工确认)

注意:模糊匹配应作为“备选手段”。实践中通常先做规范化清洗(如 trimws()、统一大小写、去除多余空格/标点),再考虑 fuzzy matching,以降低误匹配风险。

延伸阅读:建议自行检索关键词
- fuzzyjoin stringdist_left_join
- Jaro-Winkler distance
- record linkage(记录链接)

分组:group_by()
3.2.7 分组计算与汇总:group_by()summarise()

常用

在标准整洁数据形态下,按组汇总(group-by aggregation)是提取聚合特征的常规运算。
(如对比不同空间单元的平均寿命、统计特定时间节点的总人口,或执行“大洲 × 年份”的交叉层级汇总)

该类运算通常遵循“拆分-应用-合并”(Split-Apply-Combine)范式,主要包含以下两个执行步骤:

  • group_by():界定分组变量(grouping variable),即确立数据拆分与聚合的维度。
  • summarise():针对各独立数据子集执行标量计算并生成汇总指标(如计算均值、中位数、总和或观测频数等)
Figure 8. group_by() + summarise() 基本概念图

Figure 8. group_by() + summarise() 基本概念图


示例 1 按洲汇总(每个洲有多少条记录?平均寿命是多少?)

test_data %>%
  group_by(continent) %>%             # 按照洲名分组
  summarise(                          # 在分好的组内进行汇总
    n = n(),                          # 组内观测数(行数) n() 检查行数
    lifeExp_mean = mean(lifeExp),     # 平均寿命
    lifeExp_median = median(lifeExp), # 中位数寿命
    .groups = "drop"                  # 重要!取消分组-方便后续
  ) %>%
  arrange(desc(lifeExp_mean))         # 将数据重新按平均寿命降序排列
  # A tibble: 5 × 4
    continent     n lifeExp_mean lifeExp_median
    <fct>     <int>        <dbl>          <dbl>
  1 Oceania      24         74.3           73.7
  2 Europe      360         71.9           72.2
  3 Americas    300         64.7           67.0
  4 Asia        396         60.1           61.8
  5 Africa      624         48.9           47.8

示例 2 按年份汇总(全球人口总量 + 平均寿命)

test_data %>%
  group_by(year) %>%                 # 按照年份分组
  summarise(                         # 在分好的组内进行汇总
    n_country = n_distinct(country), # 当年包含了多少个国家/地区(去重计数)
    pop_total = sum(pop),            # 全球人口总和
    lifeExp_mean = mean(lifeExp),    # 当年平均寿命
    .groups = "drop"                 # 取消分组-方便后续
    ) %>%
  arrange(year) %>%
  head()
  # A tibble: 6 × 4
     year n_country  pop_total lifeExp_mean
    <int>     <int>      <dbl>        <dbl>
  1  1952       142 2406957150         49.1
  2  1957       142 2664404580         51.5
  3  1962       142 2899782974         53.6
  4  1967       142 3217478384         55.7
  5  1972       142 3576977158         57.6
  6  1977       142 3930045807         59.6

重点

示例 3 #双重分组(国家 × 年份):构建“相对全球平均”的寿命指数

# 双重分组(国家 × 年份):构建“相对全球平均”的寿命指数
# 目标:对每一年,先算全球平均寿命;再算每个国家当年的平均寿命;
# 最后计算指数 = 国家平均 / 全球平均 × 100

# 1) 当年“全球平均寿命”
global_year <- test_data %>%
  group_by(year) %>%                 # 按照年份分组
  summarise(                         # 在分好的组内进行汇总
    lifeExp_global = mean(lifeExp),  # 计算每年全球的平均寿命
    .groups = "drop"                 # 取消分组-方便后续
  )

# 2) 当年“国家/地区平均寿命”(按 国家/地区×年份)
country_year <- test_data %>%
  group_by(year, country) %>%        # 按照年份 + 国家/地区分组
  summarise(                         # 在分好的组内进行汇总
    lifeExp_country = mean(lifeExp), # 计算每年+各国的平均寿命
    .groups = "drop"                 # 取消分组-方便后续
  )

# 3) 合并后计算指数(国家相对全球 = 100 为全球平均)
lifeExp_index <- country_year %>%
  left_join(global_year, by = "year") %>% # 按照年份合并表 (后续会教)
  mutate(                                 # 创造新列,计算指数
    lifeExp_index = lifeExp_country / lifeExp_global * 100
  )

# 预览:以 2007 年为例,查看指数最高的前 10 个国家
lifeExp_index %>%
  filter(year == 2007) %>%
  arrange(desc(lifeExp_index)) %>%
  select(year, country, lifeExp_country, 
         lifeExp_global, lifeExp_index) %>%
  head(5) # 2007年各国/地区平均 vs 全球平均寿命前五名
  # A tibble: 5 × 5
     year country          lifeExp_country lifeExp_global lifeExp_index
    <int> <fct>                      <dbl>          <dbl>         <dbl>
  1  2007 Japan                       82.6           67.0          123.
  2  2007 Hong Kong, China            82.2           67.0          123.
  3  2007 Iceland                     81.8           67.0          122.
  4  2007 Switzerland                 81.7           67.0          122.
  5  2007 Australia                   81.2           67.0          121.
拆分:separate()
3.2.8 拆分合并字段:separate()(与 unite() 对照)

在真实数据中,常见的一类结构问题是:多个信息被写在同一列中(需要拆分),或需要将多个列合并为一个更便于分析与呈现的字段(需要合并)

tidyr 中,separate()unite() 分别用于完成这两类操作。


A. 拆分:把“混在一起的一列”拆成多列separate()

Figure 9. separate() 基本概念图

Figure 9. separate() 基本概念图

下面构造一个简化的 messy data 示例:将“姓名-性别-年龄”在同一列里

# 构造示例数据(messy:多个变量挤在一列)
messy_people <- tibble::tibble(
  id = 1:4,
  profile = c("Li_M_23", "Wang_F_19", "Zhang_M_31", "Chen_F_26")
)

messy_people
  # A tibble: 4 × 2
       id profile   
    <int> <chr>     
  1     1 Li_M_23   
  2     2 Wang_F_19 
  3     3 Zhang_M_31
  4     4 Chen_F_26
# 使用 separate() 拆成三列:name、sex、age
clean_people <- messy_people %>%    # 目标数据
  tidyr::separate(                  # 拆分函数
    col = profile,                  # 需要拆分的列 column
    into = c("name", "sex", "age"), # 拆分成3列 into,名字-性别-年龄
    sep = "_",                      # profile中的隔断特征是下划线‘_’
    convert = TRUE                  # 自动转换数据类型,比如新的23从文本变成了数值
    )

clean_people # 试着比较 convert = T 与 F的输出区别,注意看年龄列的数据类型
  # 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

有时候 经纬度+高程 写在一起的情况,也可以将其拆分成3列 (经度纬度高度

messy_geo <- tibble::tibble(
  place = c("A", "B", "C"),
  lon_lat_alt = c("121.47,31.23,15", "103.82,36.06,1520", "114.06,22.55,5")
  )

messy_geo %>%
  tidyr::separate(
    col  = lon_lat_alt,
    into = c("lon", "lat", "alt"),
    sep  = ",",
    convert = TRUE
)
  # A tibble: 3 × 4
    place   lon   lat   alt
    <chr> <dbl> <dbl> <int>
  1 A      121.  31.2    15
  2 B      104.  36.1  1520
  3 C      114.  22.6     5

B. 合并:把“多个列”合成一个字段unite()

Figure 10. unite() 基本概念图

Figure 10. unite() 基本概念图

当日期时间被拆成多列(year, month, day, hour ,minute ,second)时,经常需要合并为一个 timestamp 字段用于排序、绘图或建模

time_parts <- tibble::tibble( # 构建一个示例数据,可通过输入 time_parts查看
  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 %>%         # 对示例数据进行整理
  tidyr::unite(        # 合并函数
    col = "timestamp", # 需要合并成 timestamp列
    year, month, day,  # 原始数据中的列名
    hour, minute, second,
    sep = "-",         # 可以以连字符连接
    remove = FALSE     # 不删除原始数据,可以对比结果
    )
  # A tibble: 2 × 7
    timestamp          year month   day  hour minute second
    <chr>             <dbl> <dbl> <dbl> <dbl>  <dbl>  <dbl>
  1 2025-4-29-19-3-52  2025     4    29    19      3     52
  2 2025-4-30-8-45-10  2025     4    30     8     45     10
长宽转换:pivot_*()
3.2.9 长宽转换:pivot_longer()pivot_wider()

在数据整理中,数据的“形状”往往直接影响后续处理与分析的效率。常见的整理需求之一,就是在两种表格形态之间进行转换:

  1. 宽数据(Wide Data)
    列名本身包含了变量取值(如 2019、2020 作为列名)
    适合人类阅读与报表展示

  2. 长数据(Long Data)
    更符合 Tidy Data(整洁数据) 的组织原则,即“一列变量、一行观测”。
    更适合计算机处理与可视化

tidyr 中,可通过“旋转”(Pivot)系列函数实现形态转换:
pivot_longer()pivot_wider()

核心辨析:长数据 vs 宽数据

“到底谁才是整洁数据?” 答案是:长数据(Long Data)是 Tidyverse 定义的标准“整洁数据”,但在实际工作中,我们需要根据下游用途灵活切换。

  • 长数据 (Long Data)可视化 (ggplot2) 分组统计
    这是 R 语言/计算机 最喜欢的格式。只要你需要画图(例如用不同颜色代表不同年份)或者分组汇总,就必须转为长数据。

  • 宽数据 (Wide Data)报表展示 矩阵运算
    这是 人类阅读(Excel 风格)最直观的格式。此外,后续学习某些数学模型(如聚类分析)时,也必须使用这种格式作为输入。


A. 宽变长:把“列名”变成“变量” pivot_longer()

常用 这是数据清洗的第一步,通常用于把“列名里的信息”折叠进数据行里。

Figure 11. pivot_longer() 基本概念图

Figure 11. pivot_longer() 基本概念图

场景:列名是年份(1999, 2000),而不是变量名。我们需要把这些年份放到一列叫 year 的变量中

# 构造示例数据(宽数据:年份被写在列名上)
wide_data <- tibble::tibble(
  country = c("A", "B", "C"),
  `1999`  = c(745, 300, 500),  # 注意:数字作为列名需加反引号
  `2000`  = c(200, 400, 600)
)

wide_data
  # A tibble: 3 × 3
    country `1999` `2000`
    <chr>    <dbl>  <dbl>
  1 A          745    200
  2 B          300    400
  3 C          500    600
# 使用 pivot_longer() 将宽表转换为长表
long_data <- wide_data %>% 
  tidyr::pivot_longer(
    cols = c(`1999`, `2000`),   # 1) 哪些列需要“旋转”?(也可以写 !country)
    names_to  = "year",         # 2) 原来的列名(1999/2000)去哪里?-> 新变量 "year"
    values_to = "cases",        # 3) 原来的数值(745/200...)去哪里?-> 新变量 "cases"
    names_transform = list(year = as.integer) # 可选:顺便把年份转为整数
  )

long_data
  # A tibble: 6 × 3
    country  year cases
    <chr>   <int> <dbl>
  1 A        1999   745
  2 A        2000   200
  3 B        1999   300
  4 B        2000   400
  5 C        1999   500
  6 C        2000   600

注意:转换后的长表,行数增加了(3个国家 × 2个年份 = 6行),但结构满足了 Tidy Data 原则


B. 长变宽:把“变量”铺开成“列” pivot_wider()

常用

当我们需要制作便于阅读的统计表,或计算“行内差值”(如 type_A - type_B) 时,常需要将数据变宽

Figure 12. pivot_wider() 基本概念图

Figure 12. pivot_wider() 基本概念图

场景:所有的指标类型都挤在 size 列里,数值在 amount 列里。我们希望把每种 size 变成单独的一列

# 构造示例数据(长数据:指标混在一列)
pollution <- tibble::tibble(
  city = c("New York", "New York", "London", "London"),
  size = c("large", "small", "large", "small"),
  amount = c(23, 14, 22, 16)
)

pollution
  # A tibble: 4 × 3
    city     size  amount
    <chr>    <chr>  <dbl>
  1 New York large     23
  2 New York small     14
  3 London   large     22
  4 London   small     16
# 使用 pivot_wider() 将长表转换为宽表
pollution %>% 
  tidyr::pivot_wider(
    names_from  = size,    # 1) 新的列名来自哪一列?(这里的 large/small 将变成列名)
    values_from = amount   # 2) 新列的数据来自哪一列?
  )
  # A tibble: 2 × 3
    city     large small
    <chr>    <dbl> <dbl>
  1 New York    23    14
  2 London      22    16

4.0 数据清洗与质量诊断

4.0 数据清洗与诊断

4.0 数据清洗与质量诊断

清洗导航

数据质量诊断与清洗

“整洁的数据结构” 并不等同于“高质量的数据内容”
本节聚焦于解决真实数据中常见的缺失、重复、格式混乱与命名不规范等问题

💡 提示:数据清洗策略的选择往往取决于业务逻辑而非单纯的代码技巧
操作前请务必做好数据备份或使用新变量存储清洗结果

缺失值:NA
4.1 缺失值处理:NA 的识别与应对

在 R 语言环境中,缺失值通常以 NA (Not Available) 标识。该标识在运算中具有传递性(propagation):在未加干预的情况下,涉及 NA 的算术运算或统计汇总通常会直接返回 NA,进而阻断正常的数值计算与结果解析。

4.1.1 缺失值检验机制

执行缺失值诊断的基础函数主要包含 is.na()anyNA():前者用于对数据对象执行逐元素的逻辑检验并返回布尔向量,后者用于执行宏观的整体判定(即查验目标对象中是否存在缺失值,并返回单一的 TRUE/FALSE 逻辑值)

# 构造包含缺失与重复的示例数据
df_dirty <- tibble::tibble(
  id = c(1, 2, 3, 3, 4),
  value = c(10, NA, 30, 30, 50),
  category = c("A", "B", NA, NA, "C")
)

# 1. 全局诊断:数据中是否存在任何缺失值?
anyNA(df_dirty)
  [1] TRUE
# 2. 定位缺失:计算各变量的缺失数量
df_dirty %>%
  summarise(
    na_count_value = sum(is.na(value)),    # value变量/列中有多少个 NA值
    na_count_cat   = sum(is.na(category))  # category变量/列中有多少个 NA值
  )
  # A tibble: 1 × 2
    na_count_value na_count_cat
             <int>        <int>
  1              1            2

警惕:计算函数的 NA 敏感性

在计算均值、总和等统计量时,NA 会导致结果无法计算。需要使用na.rm = TRUE来规避NA值。

# 错误示范:直接计算平均数
mean(c(1, 2, NA, 4))  

# 正确做法:显式声明移除缺失值 (na.rm = TRUE)
mean(c(1, 2, NA, 4), na.rm = TRUE)

4.1.2 缺失值的处理策略

缺失值处理通常有两条基本路径:删除(Deletion)填补(Imputation)

Figure 13. drop_na() 与 replace_na() 基本概念图

Figure 13. drop_na() 与 replace_na() 基本概念图

路径 A

丢弃法(Deletion)drop_na()。适用于数据量充足的场景,且缺失模式接近完全随机(Missing Completely At Random, MCAR)时;在此条件下,删除缺失记录通常不会引入显著的样本偏差。

# 删除所有包含缺失值的行(严苛模式)
df_dirty %>% drop_na()  
  # A tibble: 2 × 3
       id value category
    <dbl> <dbl> <chr>   
  1     1    10 A       
  2     4    50 C
# 仅当 'value' 列缺失时才删除该行(保留 category 缺失的行)
df_dirty %>% drop_na(value) 
  # A tibble: 4 × 3
       id value category
    <dbl> <dbl> <chr>   
  1     1    10 A       
  2     3    30 <NA>    
  3     3    30 <NA>    
  4     4    50 C

路径 B

填补法(Imputation)replace_na() / fill()。适用于样本较为珍贵的场景,或缺失本身具有特定含义时。
(例如“未填写”可能代表“无”或“0”,需结合数据采集逻辑判断)

# 演示:对比不同的填补策略
df_dirty %>%
  mutate(
    # 策略 1: 填补为固定值 (如 0)
    # [场景] 缺失代表“没有”或“数量为0”
    val_zero   = replace_na(value, 0),
    
    # 策略 2: 填补为均值 (Mean)
    # [场景] 数据分布比较均匀 (注意:必须加 na.rm = TRUE)
    val_mean   = replace_na(value, mean(value, na.rm = TRUE)),
    
    # 策略 3: 填补为中位数 (Median)
    # [场景] 数据有极端值/偏态分布 (中位数比均值更稳健)
    val_median = replace_na(value, median(value, na.rm = TRUE)),
    
    # 策略 4: 字符型填补 (固定标签)
    # [场景] 缺失代表“未知”或“未录入”
    cat_fixed  = replace_na(category, "Unknown")
  )
  # A tibble: 5 × 7
       id value category val_zero val_mean val_median cat_fixed
    <dbl> <dbl> <chr>       <dbl>    <dbl>      <dbl> <chr>    
  1     1    10 A              10       10         10 A        
  2     2    NA B               0       30         30 B        
  3     3    30 <NA>           30       30         30 Unknown  
  4     3    30 <NA>           30       30         30 Unknown  
  5     4    50 C              50       50         50 C

进阶:使用回归模型预测填补(Regression Imputation)

进阶思路
当缺失变量与其他变量存在较强相关性(例如:身高越高,体重通常越重),简单的均值填补可能会削弱变量之间的关系结构。此时可利用“观测完整的变量”作为自变量,对“存在缺失的变量”进行预测,从而实现基于模型的填补。

# 1. 构造示例数据 (假设身高170的人体重缺失)
df <- tibble::tibble(
  height = c(160, 165, 170, 175, 180),
  weight = c(52,  58,  NA,  72,  78)  
)

# 2. 建立模型:利用现有数据训练一个线性关系 (体重 ~ 身高)
# na.action = na.exclude 保证预测时自动处理缺失索引
model <- lm(weight ~ height, data = df, na.action = na.exclude)

# 3. 预测并填补
df %>%
  mutate(
    # 计算预测值 (基于模型和当前的身高)
    pred = predict(model, newdata = .),
    
    # 逻辑:如果 weight 是 NA,就填入预测值(pred);否则保留原值
    weight_filled = ifelse(is.na(weight), pred, weight)
  )
  # A tibble: 5 × 4
    height weight  pred weight_filled
     <dbl>  <dbl> <dbl>         <dbl>
  1    160     52  51.8            52
  2    165     58  58.4            58
  3    170     NA  65              65
  4    175     72  71.6            72
  5    180     78  78.2            78

注意:该方法相较均值填补通常更精细,但预测得到的填补值会完全落在回归拟合线上,从而可能人为高估变量间的相关性。
(原因在于填补过程削弱了原始数据的随机波动与离散性)

进阶:时间序列数据的插值与平滑填补

进阶思路,了解即可
对于随时间连续变化的数据(如气温、股价、空气质量),相邻观测往往存在时间依赖性。此时可利用“前后时刻的取值”来刻画局部趋势,并据此进行插值或平滑填补。下方示例使用 zoo 包完成时间序列处理(需先安装并加载)

# 1. 构造模拟数据:10 天的空气质量指数 (AQI)
# 特征:数据有上升趋势,但中间第 4-5 天连续缺失,第 9 天单点缺失
ts_df <- tibble::tibble(  # 时间序列数据
  day = 1:10,
  aqi = c(50, 52, 55, NA, NA, 68, 72, 75, NA, 82)
)

# 2. 核心操作:利用 zoo 包进行时序处理
# 若未安装,需运行 install.packages("zoo")

ts_df %>%
  mutate(
    # 方法 A: 线性插值 (Linear Interpolation)
    # [原理] 连接缺失点前后的数值画一条直线 (局部线性回归)
    # [结果] 55 -> 59.3 -> 63.6 -> 68 (平滑过渡)
    aqi_interp = zoo::na.approx(aqi, na.rm = FALSE),
    
    # 方法 B: 移动窗口均值 (Moving Average)
    # [原理] 计算前后窗口(如3天)的平均值作为填补参考
    # 这里先计算一个“3天滑动平均线”,再用它来填补缺失位
    roll_mean  = zoo::rollapply(aqi, width = 3, FUN = mean, 
                                na.rm = TRUE, partial = TRUE, fill = NA),
    aqi_ma     = coalesce(aqi, roll_mean) # 仅在 aqi 为 NA 时,使用 roll_mean 填补
  )
  # A tibble: 10 × 5
       day   aqi aqi_interp roll_mean aqi_ma
     <int> <dbl>      <dbl>     <dbl>  <dbl>
   1     1    50       50        51     50  
   2     2    52       52        52.3   52  
   3     3    55       55        53.5   55  
   4     4    NA       59.3      55     55  
   5     5    NA       63.7      68     68  
   6     6    68       68        70     68  
   7     7    72       72        71.7   72  
   8     8    75       75        73.5   75  
   9     9    NA       78.5      78.5   78.5
  10    10    82       82        82     82
重复值:distinct()
4.2 重复值处理:distinct()

数据采集偏误、多源表横向关联不当或数据集成冗余,常会导致数据集中出现重复观测记录(duplicate records)

统计风险

重复数据会引发样本量的偏大估计(sample inflation),进而导致统计推断中的标准误(SE)被低估,增加一类错误(假阳性)的发生风险。因此,在执行统计描述或模型构建前,需明确分析粒度并进行唯一性校验


4.2.1 识别重复数据

在执行删除前,通常建议先量化重复记录的规模(重复行数/重复比例),以评估数据质量问题的严重程度,并定位可能的产生环节。

# 1. 构造示例数据
# 特征:第1行与第3行“完全重复”;第4行 id 重复但数值不同(部分重复)
df_dup <- tibble::tibble(
  id    = c("P01", "P02", "P01", "P01"), 
  date  = c("2023-10-01", "2023-10-02", "2023-10-01", "2023-10-05"),
  value = c(100, 200, 100, 150)
)

# 2. 诊断:有多少行是完全重复的?
# 逻辑:总行数 - 去重后的行数 nrow() 行数查询 输出是数字
n_dupes <- nrow(df_dup) - nrow(distinct(df_dup))

# 打印诊断结果
paste("发现完全重复的行数:", n_dupes)
  [1] "发现完全重复的行数: 1"

提示:若需要定位“具体哪些行发生重复”,可使用
group_by(所有变量) %>% filter(n() > 1)(找出完全重复的记录),或使用 janitor::get_dupes(df_dup)(需提前安装 janitor 包)


4.2.2 剔除策略

tidyverse 环境中,可使用 distinct() 进行重复记录的剔除(保留唯一记录)

Figure 14. distinct() 基本概念图

Figure 14. distinct() 基本概念图

根据“重复”的定义不同,distinct() 可对应两类常见处理策略:

策略 A

完全去重
适用于整行所有信息完全一致的冗余记录

# 默认行为:保留所有变量,剔除内容完全一致的行
# 结果:保留了 3 行 (P01 的第一条和第三条合并了)
df_dup %>% 
  distinct()
  # A tibble: 3 × 3
    id    date       value
    <chr> <chr>      <dbl>
  1 P01   2023-10-01   100
  2 P02   2023-10-02   200
  3 P01   2023-10-05   150

策略 B

按关键变量去重
适用于“同一主体存在多条记录,但当前分析要求每个主体唯一”的场景

# 逻辑:只根据 id 去重,保留第一次出现的行
# 参数 .keep_all = TRUE 至关重要,否则会丢弃除 id 外的其他列
df_dup %>% 
  distinct(id, .keep_all = TRUE)
  # A tibble: 2 × 3
    id    date       value
    <chr> <chr>      <dbl>
  1 P01   2023-10-01   100
  2 P02   2023-10-02   200

警惕:隐性数据丢失

使用策略 B.keep_all = TRUE 时,distinct() 默认保留关键变量下第一条出现的记录。若数据原始顺序不确定,最终被保留的记录可能具有随机性,从而造成“隐性”的信息丢失。

建议:在按关键变量去重前,务必先使用 arrange() 对数据进行明确排序(例如优先保留最新记录或最高质量记录)

# 规范写法:先按日期排序,确保保留的是“最早”的那一条
df_dup %>%
  arrange(date) %>% 
  distinct(id, .keep_all = TRUE)
  # A tibble: 2 × 3
    id    date       value
    <chr> <chr>      <dbl>
  1 P01   2023-10-01   100
  2 P02   2023-10-02   200
格式修正:stringr
4.3 文本清洗与数据类型修正

在多源城市数据中,非结构化文本常包含字符噪音 (如多余空格、大小写不统一);此外,数值型字段若包含特殊符号 (如货币符号、百分号),常被错误识别为字符型变量。此类格式性偏误会导致分组计算失效、关联匹配失败或算术运算报错,需在分析前执行规范化处理。

4.3.1 文本标准化:stringr

常用

stringr 包提供了处理字符串的标准化工具集。针对文本噪音,常用的清洗步骤包括去除冗余空格统一大小写规范

  • 去除首尾空格str_trim()
    用于清除字符串起始与末尾的不可见空格 (防止因空格导致完全相同的地名或标签无法匹配)

  • 转换字符大小写str_to_lower() / str_to_upper()
    用于将文本统一转换为全小写或全大写 (确保如 “London” 与 “london” 在分组统计时被视作同一类别)

# 1. 构造脏数据
# 问题:性别大小写不一、含空格;收入含符号,无法计算
df_text <- tibble::tibble(
  name   = c("Alice", " Bob", "Charlie "),
  gender = c("Female", "male", "Male "),
  income = c("$2,500", "3000", "$4,200")
)

# 2. 清洗前:直接分组计数,会发现 "Male " 和 "male" 被视作两类
df_text %>% count(gender)
  # A tibble: 3 × 2
    gender       n
    <chr>    <int>
  1 "Female"     1
  2 "Male "      1
  3 "male"       1
# 3. 清洗操作
df_clean_text <- df_text %>%
  mutate(
    # 去除首尾空白字符 (Trim Whitespace)
    name = str_trim(name),
    
    # 统一转换为小写 (或 str_to_upper 转大写)
    # [目的] 消除 "Male"/"male" 的差异
    gender = str_to_lower(str_trim(gender))
  )

# 验证清洗效果
df_clean_text %>% count(gender)
  # A tibble: 2 × 2
    gender     n
    <chr>  <int>
  1 female     1
  2 male       2
4.3.2 隐性数值转换:parse_number()

常用

当数值列包含非数字字符(如 $,%时,直接使用 as.numeric() 往往会产生大量 NA(强制转换失败)
此时可使用 readr::parse_number() 提取其中可解析的数值部分,从而完成更稳健的数值转换。

df_clean_text %>%
  mutate(
    # 错误示范:as.numeric("$2,500") 会变成 NA
    # 正确做法:提取数值核心
    income_num = parse_number(income)
  )
  # A tibble: 3 × 4
    name    gender income income_num
    <chr>   <chr>  <chr>       <dbl>
  1 Alice   female $2,500       2500
  2 Bob     male   3000         3000
  3 Charlie male   $4,200       4200

parse_number() 的原理

parse_number() 会自动忽略非数字字符(如货币符号、千分位逗号、空格等),仅提取字符串中第一个可解析的数字序列。
(例:parse_number("费用:$12,345") → 12345

列名清洗:janitor
4.4 变量名规范化:janitor::clean_names()

可选辅助工具

原始数据的变量名通常存在空格、括号或特殊字符 (如 Customer NameGDP (%)。在 R 语言环境中,引用非标准列名需频繁调用反引号 (`),这增加了代码维护成本与语法错误的潜在风险。

可利用 janitor 包提供的自动化工具对变量名进行规范化清洗。其中,clean_names() 函数能将原始变量名统一转换为小写且以底划线分隔的 snake_case 命名规范,从而提升后续代码编写与批量处理的效率。

一键清洗列名:janitor::clean_names()

janitor 包可以将所有列名统一转换为 snake_case(小写_下划线),这是 R 语言的最佳命名规范

# 构造列名不规范的数据
df_bad_names <- tibble::tibble(
  `First Name` = "John",
  `Annual Income ($)` = 50000,
  `% Change` = 0.05
)

# 一键规范化
# 需先安装:install.packages("janitor")
df_bad_names %>% 
  janitor::clean_names() %>% 
  names() # 查看清洗后的列名
  [1] "first_name"     "annual_income"  "percent_change"
# 结果对照: First Name → first_name

# Annual Income ($) → annual_income_dollar

5.0 探索性数据分析 (EDA)

5.0 探索性数据分析:理念与框架

重要

探索性数据分析 (Exploratory Data Analysis,简称 EDA 由统计学家 John W. Tukey 系统提出。EDA 不宜仅被理解为若干可视化与描述统计方法的简单组合,更应被视为一种以理解数据本身为优先的分析理念。

作为正式统计推断 (如回归分析、假设检验) 与机器学习建模之前的重要环节,EDA 的核心任务是对原始数据进行系统性诊断:识别数据质量问题,刻画分布与变异特征,检视变量之间的关系,并定位异常值与潜在偏差。其方法论强调通过归纳性的观察与比较,发现数据中的规律、异常与不确定性,从而为后续研究提供更稳妥的假设基础与建模依据。

5.0.1 EDA 的核心价值

高质量的 EDA 是保障研究结论稳健性的重要基础,其价值主要体现在以下三个方面:

  1. 数据质量诊断
    用于检验前期数据处理的有效性,识别潜在的异常值极端分布以及可能影响推断的缺失、重复或编码错误,从而提升分析样本的可信度。

  2. 单变量分布刻画
    系统描述变量的中心趋势 (如均值、中位数)离散程度 (如标准差、四分位距)分布形态 (如偏态、峰态),建立对研究对象属性的基础认识。

  3. 多变量关系探索
    初步识别变量之间的共变关系与潜在相关结构 (如线性或非线性趋势),为后续统计建模中的变量选择、函数形式设定与模型诊断提供依据。

核心场景:EDA 在学位论文中的职能

在社会科学量化研究中,描述性统计 (Descriptive Statistics) 往往作为实证分析部分的起点,是支撑研究可信度的重要环节。

其主要功能包括:

  • 评估样本代表性
    检视关键构成指标 (如性别、年龄、地区) 是否存在明显的抽样偏差或结构性缺口。

  • 验证数据逻辑性
    核查核心变量的统计特征 (如均值、标准差与取值范围) 是否符合常识与理论预期,并识别不合理取值。

  • 支撑处理决策
    为变量变换 (如对数变换)、样本筛选 (如处理离群值) 与模型设定提供必要的经验依据。


5.0.2 EDA 的执行范式

在量化研究实践中,EDA 的实施通常始于对数据的数值化概括。本节将聚焦于统计摘要 (Summary Statistics)分组汇总 (Grouped Summaries),旨在通过一组可复核的统计指标,建立对数据特征的基础认知。

需要说明的是,EDA 并不限于单变量或分组统计,也包括对变量之间关系的初步检视,例如相关性分析散点关系观察与潜在共线结构识别。但为保持本节重点清晰,此处暂以描述性统计为主。

数据可视化是 EDA 的另一大核心支柱。鉴于其知识体系的独立性与丰富性,将在 下一章 单独进行系统讲授。

一个较为标准的 EDA 迭代循环通常包含以下三个步骤:

  1. 界定问题域
    明确数据集中各变量的含义、测量尺度 (measurement scale) 及理论上合理的取值范围。
  2. 计算描述指标
    利用集中趋势 (如均值、中位数)离散程度 (如标准差、分位数) 等统计量,系统刻画数据的分布特征。
  3. 识别结构性异质
    追溯数据内部的变异来源 (heterogeneity):不同组别间的差异是否稳定?波动异常是否来自样本结构、测量误差或潜在机制差异?

接下来,将介绍如何利用 tidyverse 生态中的现代化工具 (特别是 skimr 包),高效产出更接近学术写作规范的描述性统计与汇总结果。


5.1 EDA 实战工具箱

工具导航

探索性数据分析核心工具

本节围绕探索性数据分析的基础工具展开,主要包括全貌扫描描述统计分组汇总相关性分析四个部分,用于建立对数据结构与变量关系的初步认识。

💡 提示:本节主要聚焦于数值层面的概括与比较;与后续章节中的可视化方法结合,能够形成更完整的 EDA 分析框架。

全貌扫描:Skim
5.1.1 统计概览:从 summary()skimr

在数据分析的初始阶段,建立对数据集的宏观统计认知是关键的起点。通常需要快速把握变量的集中趋势、离散程度与完整性(缺失情况),为后续的数据清洗、变量处理与建模决策提供依据。

1. 基础方案:Base R 的 summary()

在 Base R 中,summary() 是最通用的概览函数之一。对数值型变量,它提供经典的“六数概括”(Six-Number Summary),即:
最小值(Min.)、第一四分位数(1st Qu.)、中位数(Median)、均值(Mean)、第三四分位数(3rd Qu.)与最大值(Max.)

# 以 gapminder 数据为例
# pop (人口) 可能会使用科学计数法显示,想要规避可以使用 options(scipen = 9999)
options(scipen = 9999)
summary(gapminder) 
          country        continent        year         lifeExp     
   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                                                
        pop               gdpPercap       
   Min.   :     60011   Min.   :   241.2  
   1st Qu.:   2793664   1st Qu.:  1202.1  
   Median :   7023596   Median :  3531.8  
   Mean   :  29601212   Mean   :  7215.3  
   3rd Qu.:  19585222   3rd Qu.:  9325.5  
   Max.   :1318683096   Max.   :113523.1  
  

局限性:尽管 summary() 能提供基础统计量,但其纯文本输出较为单一,难以直观呈现分布形态(如偏态、长尾或双峰);同时,对缺失值 NA 的呈现也较为有限,通常缺乏占比层面的汇总描述。

2. 进阶方案:Tidyverse 生态的 skimr::skim()

为弥补基础概览的不足,可引入 skimr 包。作为面向 tidyverse 工作流的 EDA 工具,skim() 能生成结构化的统计报告,并提供迷你可视化提示,以更直观地呈现变量分布与缺失情况。

前置准备

skimr 并非 tidyverse 的核心包,需单独安装并加载。若尚未安装,可先运行 install.packages("skimr")

# 以 gapminder 数据为例
# skimr::focus()的部分可以根据实际需要调整!
gapminder %>%
  skimr::skim() %>%  # 这一步会产生很多输出,可自行阅读,下方只展示了部分
  skimr::focus(n_missing, 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
country 0
continent 0

Variable type: numeric

skim_variable n_missing mean sd hist
year 0 1979.50 17.27 ▇▅▅▅▇
lifeExp 0 59.47 12.92 ▁▆▇▇▇
pop 0 29601212.32 106157896.74 ▇▁▁▁▁
gdpPercap 0 7215.33 9857.45 ▇▁▁▁▁

skim() 的优势:

  • 结构化分层
    输出结果会按变量类型(如 factornumeric自动分块展示,便于快速定位不同类型变量的统计信息。
  • 完整性度量
    通过 n_missing(缺失数)complete_rate(完整率) 量化数据的完整性,从而为数据质量评估提供直接依据。
  • 分布提示
    numeric 模块中,hist(迷你直方图) 可在不单独作图的情况下,对分布形态提供直观提示。
    • 偏态线索
      可用于初步识别左偏/右偏(Skewness)等分布不对称现象。
    • 异常模式线索
      可辅助发现双峰(Bimodal)或潜在离群值(Outliers)等异常结构。
统计详析:Summarise
5.1.2 统计详析:定制化指标计算

在 3.2.7(分组汇总)中已初步使用 summarise()。与 skim() 的“全维度概览”不同,summarise() 允许根据具体研究问题对统计量进行定制化组合:围绕研究关注的变量与分组结构,计算更聚焦、更可解释的核心指标,从而生成更精简的描述性统计表。

1. 统计描述的核心指标体系

在实证研究中,连续变量的描述通常围绕集中趋势离散程度展开。为更准确地选择统计量,需要理解其数学定义与适用场景。

核心统计指标定义

一、集中趋势(Central Tendency):衡量数据分布的中心位置

  • 均值(Mean,符号 \(\mu\),读作 mu)
    \[\mu = \frac{1}{n}\sum_{i=1}^{n} x_i\]
    特性 使用全部样本信息,但对异常值(Outliers)敏感;更适用于近似对称分布。


  • 中位数(Median,\(M\)
    数据排序后的中心值\(50\%\) 分位数)
    特性 具有鲁棒性(Robustness),能有效抵御极值干扰;对偏态分布(如收入)尤为常用。


二、离散程度(Dispersion):度量数据的变异度或波动范围

  • 极差(Range,\(R\)
    \[R = x_{\max} - x_{\min}\]
    特性最小值(Min)最大值(Max)决定,对异常值非常敏感。


  • 标准差(Standard Deviation,符号 \(\sigma\),读作 sigma)
    \[\sigma = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (x_i - \mu)^2}\]
    特性 量化数据点偏离均值的平均距离;\(\sigma\) 越大,分布越离散。
    注:样本标准差常使用 \(n-1\) 作分母;此处给出总体形式便于理解


  • 四分位距(Interquartile Range,\(IQR\)
    \[IQR = Q_3 - Q_1\]
    特性 聚焦中间 50% 的数据范围\(Q_3\) 为 75% 分位数,\(Q_1\) 为 25% 分位数),对极端值更稳健。


2. 代码实现

利用 summarise() 配合上述统计函数,我们可以精准提取 gapminder 数据集中 lifeExp(预期寿命)的关键特征。

# 定制化计算:对比均值与中位数,评估数据偏态
gapminder %>%
  summarise(
    # 1. 集中趋势
    mean_life   = mean(lifeExp),            # 均值
    median_life = median(lifeExp),          # 中位数
    
    # 2. 离散程度
    sd_life     = sd(lifeExp),              # 标准差
    iqr_life    = IQR(lifeExp),             # 四分位距
    
    # 3. 极值范围 (Range)
    min_life    = min(lifeExp),
    max_life    = max(lifeExp),
    
    # 4. 任意分位数 (如 90% 分位数)
    p90_life    = quantile(lifeExp, 0.90)
  )
  # A tibble: 1 × 7
    mean_life median_life sd_life iqr_life min_life max_life p90_life
        <dbl>       <dbl>   <dbl>    <dbl>    <dbl>    <dbl>    <dbl>
  1      59.5        60.7    12.9     22.6     23.6     82.6     75.1

解读:若 meanmedian 差异显著(均值远大于中位数),通常暗示数据存在右偏分布

缺失值剔除规范

之前介绍过,基于 R 的运算逻辑,缺失值 (NA) 具有代数传染性:若输入向量中包含任意 NA,默认的聚合运算将直接返回 NA

为确保统计结果的有效性,在执行聚合函数(如 mean, sd, sum, max)时,必须显式设定参数 na.rm = TRUE (NA Remove),即在计算前先行剔除缺失样本。

# 规范化代码范式
summarise(
  avg_value = mean(variable, na.rm = TRUE), # 剔除 NA 后计算均值
  sd_value  = sd(variable, na.rm = TRUE)    # 剔除 NA 后计算标准差
)
分组洞察:Grouped
5.1.3 分组洞察:揭示数据的结构性差异

在 EDA 阶段,仅依赖全局统计量往往会掩盖数据内部的重要差异,这类现象可理解为“掩盖效应”。为在实证层面检视研究假设,分析视角需要从总体概览进一步转向分组比较

分析逻辑:从全局(Global) 到条件(Conditional)

科学研究的核心之一在于比较(Comparison)。借助 group_by()summarise() 的协同工作,可计算不同组别下的条件统计量,从而揭示数据的异质性(Heterogeneity)

全局视角:“所有观测的平均寿命是多少?”
分组视角:“不同大洲、不同年份的平均寿命是否存在系统性差异?”


实战示例 1:构建“基线表”

常见

在量化研究中,常需要制作一张 Table 1(基线表) 用于呈现不同组别的特征差异。为避免将不同年份的数据混杂(时间效应),此处选取 2007 年的数据作为截面进行示例分析。

以下代码展示了如何快速生成分地区的统计摘要:

# 探究:2007年不同大洲的预期寿命是否存在结构性差异?
gapminder %>%
  # 1. [关键步骤] 锁定时间截面,排除时间维度的干扰
  filter(year == 2007) %>% 
  
  # 2. 设定比较维度 
  group_by(continent) %>%
  
  # 3. 计算核心指标 
  summarise(
    n_obs     = n(),                       # 样本量 (N)
    mean_life = mean(lifeExp, na.rm=TRUE), # 均值
    sd_life   = sd(lifeExp, na.rm=TRUE),   # 标准差
    
    # [进阶技巧] 学术格式化:构建符合期刊规范的 "Mean (SD)" 格式
    # sprintf 是字符串格式化函数:
    # "%.1f" 含义:保留1位小数的浮点数 (Float)
    stats_fmt = sprintf("%.1f (%.1f)", mean_life, sd_life)
  ) %>%
  
# 4. 按均值降序排列,并仅保留汇报列
  arrange(desc(mean_life)) %>% 
  select(continent, n_obs, stats_fmt)
  # A tibble: 5 × 3
    continent n_obs stats_fmt 
    <fct>     <int> <chr>     
  1 Oceania       2 80.7 (0.7)
  2 Europe       30 77.6 (3.0)
  3 Americas     25 73.6 (4.4)
  4 Asia         33 70.7 (8.0)
  5 Africa       52 54.8 (9.6)

分析:基于 2007 年的截面数据,Oceania 的平均寿命远高于 Africa,且 Africa 组内的标准差较大(说明该地区内部国家间的发展差异最为显著)


实战示例 2:组内份额分析

进阶

在经济或人口学研究中,除了关注总体的均值差异,我们还经常需要考察数据的内部结构,即识别出哪些个体在群体中占据主导地位(Dominance)

summarise 的“降维”逻辑不同,这里我们需要保留微观个体 (如国家),并计算其在所属群体 (如大洲) 中的相对份额(Share)

以下代码展示了如何计算 2007 年各国的 GDP 占所在大洲总量的百分比:

# 探究:2007年,各国内部经济体量在所属大洲中的“权重”
gapminder %>%
  filter(year == 2007) %>%
  
  # 1. 预计算:算出每个国家的 GDP 总量 (单位:美元)
  mutate(gdp_total = gdpPercap * pop) %>%
  
  # 2. 分组:按大洲划分,为下一步“组内计算”做准备
  group_by(continent) %>%
  
  # 3. 组内占比计算 (Window Function)
  # 逻辑:个体的 GDP / 该组(大洲)所有国家 GDP 之和
  mutate(
    gdp_share = gdp_total / sum(gdp_total),
    
    # [格式构建] 目标格式:GDP总量 (占比%)
    # 步骤 A: 将 GDP 换算为 "十亿 (Billion)" 单位,保留1位小数
    gdp_val_fmt = round(gdp_total / 1e9, 1),
    
    # 步骤 B: 使用 scales 包将小数转化为百分比字符串 (如 0.25 -> "25.0%")
    share_pct_fmt = scales::percent(gdp_share, accuracy = 0.1),
    
    # 步骤 C: 拼接字符串 -> "300.5B (25.0%)"
    stats_fmt = sprintf("%sB (%s)", gdp_val_fmt, share_pct_fmt)
  ) %>%
  
  # 4. 排序:按大洲和份额降序排列
  # 这一步至关重要,决定了下一步截取的是哪几个国家
  arrange(continent, desc(gdp_share)) %>%
  
  # 5. 截取 Top N (Slicing)
  # slice_head: 专门用于选取顶部的行
  # n = 3: 因为数据处于 group_by 状态,所以是提取"每个大洲的前3名"
  slice_head(n = 3) %>% 
  
  # 6. 清理:仅保留最终的汇报列
  select(continent, country, stats_fmt)
  # A tibble: 14 × 3
  # Groups:   continent [5]
     continent country        stats_fmt       
     <fct>     <fct>          <chr>           
   1 Africa    Egypt          448B (18.8%)    
   2 Africa    South Africa   407.8B (17.1%)  
   3 Africa    Nigeria        271.9B (11.4%)  
   4 Americas  United States  12934.5B (66.6%)
   5 Americas  Brazil         1722.6B (8.9%)  
   6 Americas  Mexico         1302B (6.7%)    
   7 Asia      China          6539.5B (31.6%) 
   8 Asia      Japan          4035.1B (19.5%) 
   9 Asia      India          2722.9B (13.1%) 
  10 Europe    Germany        2650.9B (17.9%) 
  11 Europe    United Kingdom 2018B (13.6%)   
  12 Europe    France         1861.2B (12.6%) 
  13 Oceania   Australia      703.7B (87.2%)  
  14 Oceania   New Zealand    103.7B (12.8%)

分析:数据揭示了极具差异的地缘经济结构:

  1. 单极主导OceaniaAmericas 呈现出典型的单极结构。澳大利亚 (87.2%) 和美国 (66.6%) 在各自区域内拥有绝对的经济统治力。
  2. 多极均衡:相比之下,Europe 呈现出更为均衡的格局,德 (17.9%)、英 (13.6%)、法 (12.6%) 三强并立,无单一国家占据压倒性优势。
  3. 亚洲格局:2007 年的 Asia 已显现出中国 (31.6%) 的领先地位,但与日本 (19.5%) 和印度 (13.1%) 共同构成了梯度明显的“三强”态势。

学术规范:Table 1 的汇报惯例

注意

在构建基线特征表(Table 1)时,不同类型变量通常遵循不同的汇报格式:

  • 连续变量(continuous)(如年龄、收入):通常汇报为 Mean (SD)
    (代码实现:mean() 配合 sd()

  • 分类变量(categorical)(如性别、地区):通常汇报为 Count (%)
    (代码实现:n() 配合 n() / sum(n())


实战示例 3:控制变量的EDA

EDA 不仅限于单一维度的比较。通过同时引入多个分组变量(如 continent + year,我们要实现在控制 (Control) 某一变量(如时间)的情况下,观察另一变量(如地区)的净差异。

# 进阶探究:控制时间变量后,观察各大洲的经济增长趋势
gapminder %>%
  # 1. 筛选关键时间点:仅选取首尾年份进行 "前后测" 对比
  filter(year %in% c(1952, 2007)) %>% 
  
  # 2. 多维分组:同时按大洲和年份切片
  group_by(continent, year) %>%        
  
  # 3. 计算统计量
  summarise(
    mean_gdp = mean(gdpPercap, na.rm=TRUE),
    .groups = "drop"  # [建议] 显式解除分组,避免后续操作报错
  ) %>%              
  
  # 4. 宽表重塑:将年份转为列,直观展示"增长幅度"
  pivot_wider(
    names_from = year, 
    values_from = mean_gdp,
    names_prefix = "Year_" # [技巧] 添加前缀 prefix,避免列名变成纯数字
  )
  # 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.
相关分析:Correlation
5.1.4 相关性分析:从 Pearson 到 Spearman

在 EDA 阶段,除对单变量分布进行概括外,还需要进一步考察变量之间是否存在稳定的共变关系。相关分析 (Correlation Analysis) 的核心作用,正是在于用一个可量化的指标,初步描述两个变量之间的关联方向与强弱。

需要注意的是,相关并不等于因果。相关分析只能回答“两个变量是否一起变化,以及变化是否同向或反向”,并不能单独说明“一个变量是否导致另一个变量变化”。因此,在研究流程中,相关分析更适合作为关系探索建模前诊断的一部分。


1. 两类常用相关系数

在实证分析中,最常见的相关系数包括 PearsonSpearman 两类。它们都用于描述变量之间的关联程度,但适用前提并不完全相同。

核心概念:Pearson 与 Spearman 相关系数

一、Pearson 相关系数 (Pearson’s \(r\)
用于衡量两个连续变量之间的线性相关程度。其常见表达式为:

\[ 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}} \]

  • \(r > 0\):正相关
  • \(r < 0\):负相关
  • \(|r|\) 越接近 1:线性相关越强
  • \(r \approx 0\):线性相关较弱或不存在

适用场景:变量近似连续、关系大致呈线性,且不希望完全忽略原始数值差异。


二、Spearman 等级相关系数 (Spearman’s \(\rho\)
用于衡量两个变量之间的单调关系,其本质是对变量先进行排序,再计算秩之间的相关。若无并列秩,其简化形式可写为:

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

其中,\(d_i\) 表示第 \(i\) 个观测在两个变量中的秩差。

适用场景:当变量分布偏态明显、含有极端值,或关系更接近单调变化而非严格线性时,Spearman 往往更稳健。


如何初步解读相关系数?
在课程学习阶段,可先作以下经验性理解:

  • \(|r| < 0.3\):相关较弱
  • \(0.3 \le |r| < 0.5\):相关较低到中等
  • \(0.5 \le |r| < 0.7\):相关中等
  • \(|r| \ge 0.7\):相关较强

注意:这些阈值仅用于初步描述,具体解释仍需结合研究问题、样本规模与变量性质。


2. 基础实现:cor()cor.test()

在 R 中,cor() 用于计算相关系数,cor.test() 则可进一步提供检验结果 (如 p 值与置信区间)。为避免时间维度对关系判断造成干扰,以下示例仍以 2007 年gapminder 截面数据为例。

library(tidyverse)
library(gapminder)

# 选取 2007 年截面,并构造 log GDP 变量
gap_2007 <- gapminder %>%
  filter(year == 2007) %>%
  mutate(log_gdpPercap = log10(gdpPercap))

# 计算 Pearson 与 Spearman 相关系数
gap_2007 %>%
  summarise(
    pearson_r  = cor(log_gdpPercap, lifeExp, method = "pearson"),
    spearman_r = cor(log_gdpPercap, lifeExp, method = "spearman")
  )
  # A tibble: 1 × 2
    pearson_r spearman_r
        <dbl>      <dbl>
  1     0.809      0.857
# Pearson 相关检验
cor.test(
  ~ log_gdpPercap + lifeExp,
  data = gap_2007,
  method = "pearson"
)
  
    Pearson's product-moment correlation
  
  data:  log_gdpPercap and lifeExp
  t = 16.283, df = 140, p-value < 0.00000000000000022
  alternative hypothesis: true correlation is not equal to 0
  95 percent confidence interval:
   0.7433069 0.8592085
  sample estimates:
        cor 
  0.8089803
# Spearman 相关检验
cor.test(
  ~ log_gdpPercap + lifeExp,
  data = gap_2007,
  method = "spearman"
)
  
    Spearman's rank correlation rho
  
  data:  log_gdpPercap and lifeExp
  S = 68434, p-value < 0.00000000000000022
  alternative hypothesis: true rho is not equal to 0
  sample estimates:
        rho 
  0.8565899

说明:这里对 gdpPercap 进行对数化处理,是因为其分布通常具有明显的右偏特征。对数变换后,更有助于观察其与 lifeExp 之间的总体关系。


3. 如何书写相关分析结果?

在研究写作中,相关分析通常需要同时交代方向强度显著性

根据当前结果,log(gdpPercap)lifeExp 的相关系数分别为:Pearson = 0.809Spearman = 0.857。这表明两者之间存在较强的正相关关系,且这一关系在线性层面与等级排序层面都较为稳定。

例如,可写为:

在 2007 年的 gapminder 截面数据中,log(gdpPercap)lifeExp 呈现较强的正相关。Pearson 相关系数为 0.809,说明两者在线性层面存在明显的正向关系;Spearman 相关系数为 0.857,且结果与 Pearson 相近,说明这一关系在排序意义上同样稳定。

若需要更精简的学术表达,也可写为:

lifeExp was positively correlated with log(gdpPercap) (Pearson’s \(r\) = 0.809; Spearman’s \(\rho\) = 0.857).

解释相关时的常见误区

  • 相关不等于因果
    即使两个变量高度相关,也不能直接得出因果结论。

  • 显著不等于重要
    在样本量较大时,即使相关系数不高,也可能得到很小的 p 值。解释时仍应优先关注效应大小

  • 零相关不等于无关系
    当变量之间存在明显的非线性结构时,Pearson 可能接近 0,但这并不意味着两者完全无关。


4. 多变量情境:相关矩阵与 corrplot

当研究涉及多个数值变量时,往往需要先计算一个相关矩阵,以观察整体关系结构。此时可先使用 cor() 得到矩阵,再根据需要借助 corrplot 包进行初步展示。

# 构造数值变量子集
gap_num <- gap_2007 %>%
  select(lifeExp, pop, gdpPercap, log_gdpPercap)

# 计算相关矩阵
cor_mat <- cor(gap_num, method = "pearson")
cor_mat
                   lifeExp         pop  gdpPercap log_gdpPercap
  lifeExp       1.00000000  0.04755312  0.6786624    0.80898025
  pop           0.04755312  1.00000000 -0.0556756   -0.02353029
  gdpPercap     0.67866240 -0.05567560  1.0000000    0.87510881
  log_gdpPercap 0.80898025 -0.02353029  0.8751088    1.00000000
library(corrplot) # 注意安装corrplot包

corrplot(cor_mat, method = "number", type = "upper")

说明corrplot 是相关矩阵展示中较常见的工具,但由于本章重点仍在数值摘要与关系识别,这里仅作方法引介。其更完整的图形表达与美化方式,可结合后续的数据可视化章节进一步理解。

6.0 本章练习

6.0 动手实战:社会调查数据分析

实战目标

本节练习将围绕真实的社会调查数据 (Survey Data) 展开。所使用的数据为 tidyverse 内置的美国综合社会调查 (General Social Survey, GSS) 示例数据 gss_cat

通过本节练习,将进一步巩固以下几项核心技能:

全貌扫描 (Overview Scan)
使用 skimr 快速把握变量分布与缺失情况。
分组洞察 (Grouped Summaries)
使用 group_by()summarise() 比较不同社会群体之间的差异。
份额分析 (Share Analysis)
结合 mutate() 计算特定群体在总体中的相对占比。
结果解读 (Result Interpretation)
学习如何基于 tibble 输出提炼并表述基本的社会学发现。

6.1 基础任务:复现“婚姻状况与媒介消费”

本任务属于一个按步骤复现的练习。分析目标是考察:婚姻状况 marital 是否与个体的日均看电视时长 tvhours存在差异。

6.1.1 任务要求

请在你的 R Project 中新建一个 R Markdown 文档:

  • 文件名建议:Lab2_Social_Survey.Rmd
  • 核心要求
    1. 加载 tidyverseskimr
    2. 调用 gss_cat 数据集 (无需额外下载;加载 tidyverse 后即可直接使用)
    3. 分析不同婚姻状态 marital下,人们每日看电视时长 tvhours的平均水平;
    4. 注意tvhours 中包含缺失值 NA,计算时需考虑缺失值处理。
6.1.2 操作步骤

Step 1
新建文档:创建新的 Rmd 文件,保留 YAML 头部,删除默认正文内容。

Step 2
复制模板:将下方提供的代码块完整复制到文档中。

Step 3
运行与观察:点击 Knit 生成输出结果。重点阅读 summarise() 所返回的结果表,并尝试解释不同婚姻状态之间的差异。

6.1.3 可复现模版
---
title: "GSS调查报告:婚姻与媒介消费"
author: "【你的名字】"
date: "`r​ Sys.Date()`"
output: html_document
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
library(tidyverse)
library(skimr)
```
## 1. 数据概览 (Data Overview)
本研究使用美国综合社会调查 (`GSS`) 数据。
首先对数据进行全貌扫描,特别是关注 `tvhours` (电视时长) 的缺失情况。
```{r}
# 1. 查看数据结构
# gss_cat 是 tidyverse 自带的数据集
glimpse(gss_cat)

# 2. 重点检查关键变量
# skim() 能让我们直观看到 tvhours 有多少 NA (n_missing)
gss_cat %>% 
  select(marital, age, tvhours) %>% 
  skim()
```
## 2. 分组差异分析 (Group Comparison)
我们探究不同婚姻状况 (`Marital Status`) 的受访者,其日均电视消费是否存在差异。
```{r}
# 计算统计量
result_table <- gss_cat %>%
  # 1. 分组:按婚姻状况
  group_by(marital) %>%
  
  # 2. 统计汇总
  summarise(
    n_sample  = n(),                       # 样本量
    mean_tv   = mean(tvhours, na.rm=TRUE), # 平均看电视时长 (注意 na.rm)
    sd_tv     = sd(tvhours, na.rm=TRUE)    # 标准差
  ) %>%
  
  # 3. 整理:保留1位小数,并按时长降序排列
  mutate(
    mean_tv = round(mean_tv, 1),
    sd_tv   = round(sd_tv, 1)
  ) %>%
  arrange(desc(mean_tv))

# 直接打印结果 (Tibble)
result_table
```
>**简要分析**: 观察结果可以发现:
>
>丧偶 (`Widowed`) 群体的日均看电视时间最长(*3.9小时*),这可能与该群体平均年龄较大、居家时间较多有关。
>
>已婚 (`Married`) 群体的电视消费时间最短(2.7小时),显著低于 从不结婚 (`Never married`) 和 离婚 (`Divorced`) 群体。这可能反映了家庭互动或育儿责任挤占了个人媒介消费时间。

6.2 实践任务:独立探索“宗教与年龄结构”

这是一个不提供现成代码的练习。
你需要综合运用本章所学的分组统计缺失值处理组内计算技能,独立完成分析任务。


6.2.1 任务说明

课题方向
考察美国不同宗教信仰群体 relig年龄结构 age

建议文件名
Lab2_Religion_Age.Rmd

研究背景
在人口社会学研究中,宗教信仰不仅体现个体的价值取向,也常与人口结构特征相关联。本练习假设,不同宗教群体在年龄结构上可能存在一定程度的异质性 (Structural Heterogeneity)。例如,部分传统宗教群体可能呈现较明显的老龄化特征,而无信仰群体则可能相对更年轻。请使用 gss_cat 数据,并借助 EDA 方法对这一构想进行初步检验。


6.2.2 硬性要求(Checklist)

你的 R Markdown 报告需完整体现以下分析流程 (请自行编写代码)

1. 数据筛选与清洗 (Data Preparation)

  • 缺失值处理:确保分析中使用的 age 数据不包含缺失值 NA
  • 总体初探:计算各宗教群体的总样本量,并仅保留样本量最大的前 5 个宗教类别进入后续分析。


2. 变量重构 (Variable Construction)

  • 基于 age 构建新的分类变量 age_group

  • 分组标准
    结合 GSS 样本特征 (如 Min = 18, Median = 47 与常见人口学划分,可采用以下方案:

    • 青年 (Young Adult):18–34 岁
    • 中年 (Middle Age):35–64 岁
    • 老年 (Senior):65 岁及以上


3. 多维结构分析 (Structural Analysis)

  • 核心要求:同时按宗教 relig代际组别 age_group进行统计。

  • 结果表中需包含以下指标

    • Group Mean Age:该宗教群体的总体平均年龄
      (注意:这里指该宗教全部样本的平均年龄,而不是某一年龄段内部的平均值)
    • N:该宗教在对应代际组别中的人数
    • Share (%):该代际人数占该宗教总人数的比例
      (即组内占比,建议以百分比表示)


4. 最终产出 (Output)

  • 排序逻辑:结果表应先按宗教排序,再按代际顺序排序 (青年 → 中年 → 老年)
  • 呈现方式:直接通过 knit 输出整洁的 tibble 表格即可,无需额外使用 kable 等方式进行美化。

6.2.3 结果验证

请根据输出结果,在文档中简要回答以下问题 (建议使用 > 引用块格式)

  • 总体特征:在前 5 个宗教群体中,平均年龄最高与最低的分别是哪两个群体?
  • 结构验证:比较无信仰 None新教 Protestant的代际构成:
    • 二者在青年组 (Young Adult)中的占比 Share是否存在明显差异?
    • 这一结果是否支持前文关于“传统宗教更偏老龄化、无信仰群体更偏年轻化”的初步假设?

6.3 进阶任务:定基指数分析

gss_cat 数据包含 2000–2014 年 的时间信息。为减少不同群体原始数值差异带来的影响,本练习引入定基指数 (Index Number) 的思路,将 2000 年 设定为基期 (Base Year = 100),用于观察后续年份的相对变化幅度。

趋势分析:种族媒介消费指数
若将 2000 年的水平设为 100,那么 2008、2010 与 2014 年各群体的指标是上升至 120,还是下降至 80?

任务要求(Checklist)

1. 分组与聚合

  • raceyear 分组,计算各族群各年份的平均电视观看时长 (需使用变量 tvhours,结果命名为 mean_tv

2. 计算定基指数 (Calculate Index in Long Format)

  • 保持长表格式不变,利用 group_by(race)mutate() 创建新变量 tv_index
  • 计算逻辑

\[ Index = \frac{\text{当年均值 }}{\text{2000 年均值}} \times 100 \]

  • 代码提示:需要提取组内 2000 年 的均值作为分母。
    • 方法 A (推荐)mean_tv / mean_tv[year == 2000] * 100
    • 方法 B (若年份已排序)mean_tv / first(mean_tv) * 100

3. 宽表展示

将计算后的 tv_index 转换为宽表。

  • race
  • year
  • tv_index (建议使用 round() 不保留小数)
  • 目标:生成一张类似“走势表”的矩阵,其中 2000 年 对应的指数应全部为 100

4. 结果解读 请重点观察 2014 年 对应的指数列,并回答以下问题:

  • 相对增长最快的是哪个种族? (指数 > 100 且数值最大)
  • 相对下降最明显的是哪个种族? (指数 < 100 且数值最小)
  • 这一结论与直接比较绝对时长时得到的判断是否一致?