数据清洗与预处理是数据分析中最重要但也最耗时的环节。据统计,数据科学家约80%的时间都花在数据清洗上。本教程将系统介绍R语言中数据清洗与预处理的各项技术,帮助你高效地处理各种数据问题。
本教程涵盖以下内容:
| 章节 | 主题 | 核心内容 |
|---|---|---|
| 第一章 | 数据探索与概览 | 了解数据的基本情况 |
| 第二章 | 缺失值处理 | 识别和处理缺失数据 |
| 第三章 | 重复值与异常值处理 | 数据质量问题解决 |
| 第四章 | 数据类型转换与变量重编码 | 数据格式规范化 |
| 第五章 | 数据筛选与排序 | 数据子集选择 |
| 第六章 | 数据变换与计算 | 创建新变量和汇总统计 |
| 第七章 | 数据合并与连接 | 多数据源整合 |
| 第八章 | 数据重塑 | 长宽格式转换 |
| 第九章 | 字符与日期处理 | 特殊数据类型处理 |
| 第十章 | 分组与聚合操作 | 分组计算 |
| 第十一章 | 数据采样与分割 | 数据集划分 |
| 第十二章 | 数据清洗实战技巧 | 综合应用 |
在Rstudio中安装本节课所有需要的包,运行以下指令:
install.packages(c(
"dplyr", "tidyr", "psych", "VIM", "visdat",
"janitor", "zoo", "mice", "DescTools", "forcats",
"Hmisc", "caret", "rsample", "caTools", "fastDummies",
"data.table", "fuzzyjoin", "stringr", "lubridate",
"xts", "readxl", "writexl", "DBI", "tidylog",
"validate", "assertthat"
))
在开始任何数据分析之前,我们首先需要对数据有一个整体的了解。本章介绍各种数据探索方法,帮助你快速掌握数据的基本情况。
了解数据的大小是第一步,我们需要知道数据有多少行(观测)和多少列(变量)。
# 加载必要的包
library(dplyr)
# 创建一个示例数据框
# 这个数据框模拟了一个简单的学生信息表
student_data <- data.frame(
id = 1:10, # 学生ID,从1到10
name = c("张三", "李四", "王五", "赵六", "钱七",
"孙八", "周九", "吴十", "郑一", "陈二"), # 学生姓名
age = c(20, 21, 19, 22, 20, 21, 19, 23, 20, 21), # 年龄
score = c(85, 90, 78, 92, 88, 76, 95, 89, 82, 91), # 成绩
gender = c("M", "F", "M", "F", "M", "F", "M", "F", "M", "F") # 性别
)
# dim()函数返回数据的维度(行数和列数)
# 第一个数字是行数,第二个数字是列数
dim(student_data)
## [1] 10 5
# nrow()函数只返回行数(观测数量)
nrow(student_data)
## [1] 10
# ncol()函数只返回列数(变量数量)
ncol(student_data)
## [1] 5
小结:dim()、nrow()、ncol()是最基础的数据维度查看函数,可以快速了解数据规模。
数据结构告诉我们每个变量的类型(数值型、字符型、因子型等)以及数据的整体组织方式。
# str()函数显示数据的详细结构
# 包括:变量名、变量类型、前几个值
str(student_data)
## 'data.frame': 10 obs. of 5 variables:
## $ id : int 1 2 3 4 5 6 7 8 9 10
## $ name : chr "张三" "李四" "王五" "赵六" ...
## $ age : num 20 21 19 22 20 21 19 23 20 21
## $ score : num 85 90 78 92 88 76 95 89 82 91
## $ gender: chr "M" "F" "M" "F" ...
# glimpse()函数来自dplyr包,显示更紧凑的结构信息
# 它以横向方式展示,更适合宽表格
glimpse(student_data)
## Rows: 10
## Columns: 5
## $ id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
## $ name <chr> "张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十", "郑一", "陈二"…
## $ age <dbl> 20, 21, 19, 22, 20, 21, 19, 23, 20, 21
## $ score <dbl> 85, 90, 78, 92, 88, 76, 95, 89, 82, 91
## $ gender <chr> "M", "F", "M", "F", "M", "F", "M", "F", "M", "F"
小结:str()是R内置函数,glimpse()来自dplyr包,两者都能快速了解数据结构。
知道变量名称可以帮助我们后续选择和操作特定列。
# names()函数返回所有变量名称(列名)
names(student_data)
## [1] "id" "name" "age" "score" "gender"
# colnames()函数与names()功能相同,专门用于数据框
colnames(student_data)
## [1] "id" "name" "age" "score" "gender"
# rownames()函数返回行名
rownames(student_data)
## [1] "1" "2" "3" "4" "5" "6" "7" "8" "9" "10"
小结:names()和colnames()可以互换使用,都能获取列名。
快速预览数据的前几行或后几行,可以了解数据的实际内容。
# head()函数默认显示前6行
head(student_data)
## id name age score gender
## 1 1 张三 20 85 M
## 2 2 李四 21 90 F
## 3 3 王五 19 78 M
## 4 4 赵六 22 92 F
## 5 5 钱七 20 88 M
## 6 6 孙八 21 76 F
# 可以指定显示的行数,例如显示前3行
head(student_data, n = 3)
## id name age score gender
## 1 1 张三 20 85 M
## 2 2 李四 21 90 F
## 3 3 王五 19 78 M
# tail()函数默认显示后6行
tail(student_data)
## id name age score gender
## 5 5 钱七 20 88 M
## 6 6 孙八 21 76 F
## 7 7 周九 19 95 M
## 8 8 吴十 23 89 F
## 9 9 郑一 20 82 M
## 10 10 陈二 21 91 F
# 显示后4行
tail(student_data, n = 4)
## id name age score gender
## 7 7 周九 19 95 M
## 8 8 吴十 23 89 F
## 9 9 郑一 20 82 M
## 10 10 陈二 21 91 F
小结:head()和tail()是快速预览数据的利器,建议在导入数据后立即使用。
摘要统计帮助我们了解数值变量的分布情况,包括最小值、最大值、均值、中位数等。
# summary()函数提供每个变量的基本统计摘要
# 对于数值变量:显示最小值、四分位数、均值、最大值
# 对于字符变量:显示长度和类型
summary(student_data)
## id name age score
## Min. : 1.00 Length:10 Min. :19.0 Min. :76.00
## 1st Qu.: 3.25 Class :character 1st Qu.:20.0 1st Qu.:82.75
## Median : 5.50 Mode :character Median :20.5 Median :88.50
## Mean : 5.50 Mean :20.6 Mean :86.60
## 3rd Qu.: 7.75 3rd Qu.:21.0 3rd Qu.:90.75
## Max. :10.00 Max. :23.0 Max. :95.00
## gender
## Length:10
## Class :character
## Mode :character
##
##
##
# describe()函数来自psych包,提供更详细的统计信息
# 包括:变量数量、均值、标准差、中位数、偏度、峰度等
# 如果没有安装psych包,先运行:install.packages("psych")
library(psych)
describe(student_data)
## student_data
##
## 5 Variables 10 Observations
## --------------------------------------------------------------------------------
## id
## n missing distinct Info Mean pMedian Gmd .05
## 10 0 10 1 5.5 5.5 3.667 1.45
## .10 .25 .50 .75 .90 .95
## 1.90 3.25 5.50 7.75 9.10 9.55
##
## Value 1 2 3 4 5 6 7 8 9 10
## Frequency 1 1 1 1 1 1 1 1 1 1
## Proportion 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
## --------------------------------------------------------------------------------
## name
## n missing distinct
## 10 0 10
##
## Value 陈二 李四 钱七 孙八 王五 吴十 张三 赵六 郑一 周九
## Frequency 1 1 1 1 1 1 1 1 1 1
## Proportion 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
## --------------------------------------------------------------------------------
## age
## n missing distinct Info Mean pMedian Gmd
## 10 0 5 0.945 20.6 20.5 1.467
##
## Value 19 20 21 22 23
## Frequency 2 3 3 1 1
## Proportion 0.2 0.3 0.3 0.1 0.1
## --------------------------------------------------------------------------------
## score
## n missing distinct Info Mean pMedian Gmd .05
## 10 0 10 1 86.6 87 7.333 76.90
## .10 .25 .50 .75 .90 .95
## 77.80 82.75 88.50 90.75 92.30 93.65
##
## Value 76 78 82 85 88 89 90 91 92 95
## Frequency 1 1 1 1 1 1 1 1 1 1
## Proportion 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
## --------------------------------------------------------------------------------
## gender
## n missing distinct
## 10 0 2
##
## Value F M
## Frequency 5 5
## Proportion 0.5 0.5
## --------------------------------------------------------------------------------
小结:summary()是R内置函数,describe()来自psych包,提供更丰富的统计信息。
缺失值是数据清洗中常见的问题,首先需要了解数据中缺失值的整体情况。
# 创建一个包含缺失值的数据框
# 用于演示缺失值处理
data_with_na <- data.frame(
id = 1:8,
value1 = c(10, NA, 30, 40, NA, 60, 70, 80), # value1有2个缺失值
value2 = c(NA, 2, 3, NA, 5, 6, NA, 8), # value2有3个缺失值
category = c("A", "B", NA, "A", "B", NA, "A", "B") # category有2个缺失值
)
# is.na()函数检测每个值是否为缺失值
# 返回一个与原数据维度相同的逻辑矩阵
is.na(data_with_na)
## id value1 value2 category
## [1,] FALSE FALSE TRUE FALSE
## [2,] FALSE TRUE FALSE FALSE
## [3,] FALSE FALSE FALSE TRUE
## [4,] FALSE FALSE TRUE FALSE
## [5,] FALSE TRUE FALSE FALSE
## [6,] FALSE FALSE FALSE TRUE
## [7,] FALSE FALSE TRUE FALSE
## [8,] FALSE FALSE FALSE FALSE
# sum(is.na())计算缺失值的总数量
# TRUE被当作1,FALSE被当作0
sum(is.na(data_with_na))
## [1] 7
# colSums(is.na())计算每列的缺失值数量
colSums(is.na(data_with_na))
## id value1 value2 category
## 0 2 3 2
# 计算每列缺失值的比例
# 用每列缺失值数量除以总行数
colSums(is.na(data_with_na)) / nrow(data_with_na)
## id value1 value2 category
## 0.000 0.250 0.375 0.250
小结:is.na()检测缺失值,配合sum()和colSums()可以快速统计缺失情况。
可视化方法可以更直观地展示缺失值的分布模式。
# VIM包提供了丰富的缺失值可视化功能
# 如果没有安装,先运行:install.packages("VIM")
library(VIM)
# aggr()函数绘制缺失值聚合图
# 左图显示每列缺失比例,右图显示缺失值组合模式
aggr(data_with_na,
numbers = TRUE, # 显示具体数值
prop = TRUE, # 显示比例而非数量
combined = FALSE) # 分开展示两个图
# visdat包提供了另一种可视化方式
# 如果没有安装,先运行:install.packages("visdat")
library(visdat)
# vis_miss()函数创建缺失值热力图
# 黑色表示缺失,灰色表示存在
vis_miss(data_with_na)
# vis_dat()函数显示数据类型和缺失情况
vis_dat(data_with_na)
小结:VIM包和visdat包提供了直观的缺失值可视化方法,推荐在数据探索阶段使用。
了解数值变量的分布情况,可以帮助发现异常值和数据特征。
# hist()函数绘制直方图,展示数值分布
# main参数设置标题,xlab参数设置x轴标签
hist(student_data$score,
main = "学生成绩分布直方图",
xlab = "成绩",
col = "lightblue",
breaks = 5) # breaks设置分组数量
# boxplot()函数绘制箱线图
# 可以直观展示中位数、四分位数和异常值
boxplot(student_data$score,
main = "学生成绩箱线图",
ylab = "成绩",
col = "lightgreen")
# 按性别分组绘制箱线图
# 公式写法:y ~ group
boxplot(score ~ gender, data = student_data,
main = "不同性别学生成绩分布",
xlab = "性别",
ylab = "成绩",
col = c("lightblue", "lightpink"))
小结:直方图展示整体分布,箱线图展示分布特征和异常值,两者配合使用效果更好。
了解每个变量的数据类型是数据处理的基础。
# class()函数返回对象的类
class(student_data) # 数据框类型
## [1] "data.frame"
class(student_data$name) # 字符型
## [1] "character"
class(student_data$age) # 数值型(整数)
## [1] "numeric"
class(student_data$score) # 数值型(实数)
## [1] "numeric"
# typeof()函数返回对象在内存中的存储类型
# 比class()更底层
typeof(student_data$id) # "integer"整数
## [1] "integer"
typeof(student_data$score) # "double"双精度浮点数
## [1] "double"
typeof(student_data$name) # "character"字符
## [1] "character"
# 查看所有列的类型
sapply(student_data, class)
## id name age score gender
## "integer" "character" "numeric" "numeric" "character"
小结:class()返回高级类型,typeof()返回底层存储类型,两者配合使用可以全面了解数据类型。
| 函数 | 用途 | 返回值 |
|---|---|---|
dim() |
查看维度 | 行数和列数 |
nrow() |
查看行数 | 行数 |
ncol() |
查看列数 | 列数 |
str() |
查看结构 | 变量类型和前几值 |
glimpse() |
查看结构(dplyr) | 紧凑的结构信息 |
names() |
查看列名 | 列名向量 |
head() |
查看前几行 | 前n行数据 |
tail() |
查看后几行 | 后n行数据 |
summary() |
摘要统计 | 统计摘要 |
is.na() |
检测缺失值 | 逻辑矩阵 |
class() |
查看类型 | 对象类 |
typeof() |
查看存储类型 | 存储类型 |
缺失值是现实数据中最常见的问题之一。正确处理缺失值对于保证分析结果的准确性至关重要。本章介绍缺失值的识别、统计和各种处理方法。
首先,我们需要识别数据中的缺失值位置。
# 创建一个包含多种缺失情况的示例数据
sales_data <- data.frame(
product = c("A", "B", "C", "D", "E", "F", "G", "H"),
price = c(100, NA, 200, 150, NA, 180, 220, NA),
quantity = c(10, 20, NA, 15, 25, NA, 30, 12),
revenue = c(1000, NA, NA, 2250, 3000, NA, 6600, NA)
)
# is.na()检测每个值是否为NA
# 返回逻辑向量,TRUE表示缺失
is.na(sales_data$price)
## [1] FALSE TRUE FALSE FALSE TRUE FALSE FALSE TRUE
# which()配合is.na()找出缺失值的位置索引
which(is.na(sales_data$price))
## [1] 2 5 8
# complete.cases()函数识别完整的行(没有缺失值的行)
# 返回逻辑向量,TRUE表示该行完整
complete.cases(sales_data)
## [1] TRUE FALSE FALSE TRUE FALSE FALSE TRUE FALSE
# 查看完整的行
sales_data[complete.cases(sales_data), ]
## product price quantity revenue
## 1 A 100 10 1000
## 4 D 150 15 2250
## 7 G 220 30 6600
# 查看有缺失值的行
# 用感叹号取反
sales_data[!complete.cases(sales_data), ]
## product price quantity revenue
## 2 B NA 20 NA
## 3 C 200 NA NA
## 5 E NA 25 3000
## 6 F 180 NA NA
## 8 H NA 12 NA
小结:is.na()检测单个值,complete.cases()识别完整行,两者配合可以精确定位缺失值。
了解缺失值的数量和比例,有助于决定处理策略。
# 计算每列缺失值数量
colSums(is.na(sales_data))
## product price quantity revenue
## 0 3 2 4
# 计算每列缺失值比例
colSums(is.na(sales_data)) / nrow(sales_data)
## product price quantity revenue
## 0.000 0.375 0.250 0.500
# 计算每行缺失值数量
rowSums(is.na(sales_data))
## [1] 0 2 2 0 1 2 0 2
# 计算总缺失值数量
sum(is.na(sales_data))
## [1] 9
# 计算总缺失值比例
sum(is.na(sales_data)) / (nrow(sales_data) * ncol(sales_data))
## [1] 0.28125
# 创建一个缺失值统计汇总表
missing_summary <- data.frame(
variable = names(sales_data),
missing_count = colSums(is.na(sales_data)),
missing_percent = round(colSums(is.na(sales_data)) / nrow(sales_data) * 100, 2)
)
missing_summary
## variable missing_count missing_percent
## product product 0 0.0
## price price 3 37.5
## quantity quantity 2 25.0
## revenue revenue 4 50.0
小结:统计缺失值数量和比例,可以帮助判断哪些变量需要重点处理。
当缺失值较少时,可以直接删除含有缺失值的行或列。
# na.omit()函数删除含有缺失值的行
# 返回只包含完整行的数据框
na.omit(sales_data)
## product price quantity revenue
## 1 A 100 10 1000
## 4 D 150 15 2250
## 7 G 220 30 6600
# 使用dplyr包的drop_na()函数
# 功能更灵活,可以指定列
library(dplyr)
# 删除所有含有缺失值的行
drop_na(sales_data)
## drop_na: removed 5 rows (62%), 3 rows remaining
## product price quantity revenue
## 1 A 100 10 1000
## 2 D 150 15 2250
## 3 G 220 30 6600
# 只删除price列有缺失值的行
drop_na(sales_data, price)
## drop_na: removed 3 rows (38%), 5 rows remaining
## product price quantity revenue
## 1 A 100 10 1000
## 2 C 200 NA NA
## 3 D 150 15 2250
## 4 F 180 NA NA
## 5 G 220 30 6600
# 删除price和quantity列都有缺失值的行
drop_na(sales_data, c(price, quantity))
## drop_na: removed 5 rows (62%), 3 rows remaining
## product price quantity revenue
## 1 A 100 10 1000
## 2 D 150 15 2250
## 3 G 220 30 6600
小结:na.omit()简单直接,drop_na()更灵活,可以根据需要选择使用。
有时数据中存在完全为空的行或列,需要清理。
# 创建一个含有全空行和列的数据框
messy_data <- data.frame(
x = c(1, NA, 3, NA),
y = c(NA, NA, NA, NA), # 全为NA的列
z = c(5, 6, NA, 8)
)
messy_data <- rbind(messy_data, c(NA, NA, NA)) # 添加全为NA的行
# janitor包提供了便捷的清理函数
# 如果没有安装,先运行:install.packages("janitor")
library(janitor)
# remove_empty()删除全为NA的行和列
remove_empty(messy_data)
## value for "which" not specified, defaulting to c("rows", "cols")
## x z
## 1 1 5
## 2 NA 6
## 3 3 NA
## 4 NA 8
# 只删除全为NA的列
remove_empty(messy_data, which = "cols")
## x z
## 1 1 5
## 2 NA 6
## 3 3 NA
## 4 NA 8
## 5 NA NA
# 只删除全为NA的行
remove_empty(messy_data, which = "rows")
## x y z
## 1 1 NA 5
## 2 NA NA 6
## 3 3 NA NA
## 4 NA NA 8
小结:janitor::remove_empty()可以快速清理空白行列,非常实用。
当缺失值较多时,删除会损失大量信息,此时可以考虑填补。
# 创建一个用于演示填补的数据
fill_data <- data.frame(
id = 1:10,
value = c(10, NA, 30, 25, NA, 35, 40, NA, 45, 50)
)
# 计算均值(忽略缺失值)
mean_value <- mean(fill_data$value, na.rm = TRUE)
mean_value
## [1] 33.57143
# 用均值填补缺失值
fill_data$value_mean <- ifelse(
is.na(fill_data$value), # 条件:是否为缺失值
mean_value, # 如果是,用均值填补
fill_data$value # 如果不是,保持原值
)
# 用中位数填补
median_value <- median(fill_data$value, na.rm = TRUE)
fill_data$value_median <- ifelse(
is.na(fill_data$value),
median_value,
fill_data$value
)
# 查看填补结果
fill_data
## id value value_mean value_median
## 1 1 10 10.00000 10
## 2 2 NA 33.57143 35
## 3 3 30 30.00000 30
## 4 4 25 25.00000 25
## 5 5 NA 33.57143 35
## 6 6 35 35.00000 35
## 7 7 40 40.00000 40
## 8 8 NA 33.57143 35
## 9 9 45 45.00000 45
## 10 10 50 50.00000 50
# 对于分类变量,可以用众数填补
# 创建一个含有缺失值的分类变量
category_data <- c("A", "B", "A", NA, "A", "B", NA, "A")
# 计算众数(出现次数最多的值)
# 使用table()统计频次
freq_table <- table(category_data, useNA = "ifany")
mode_value <- names(freq_table[which.max(freq_table)])
mode_value
## [1] "A"
# 用众数填补
category_data_filled <- ifelse(is.na(category_data), mode_value, category_data)
category_data_filled
## [1] "A" "B" "A" "A" "A" "B" "A" "A"
小结:均值适合正态分布数据,中位数适合偏态分布,众数适合分类变量。
时间序列数据中,常用前后值来填补缺失值。
# 创建时间序列数据
time_data <- data.frame(
day = 1:10,
value = c(10, NA, NA, 40, 50, NA, 70, 80, NA, 100)
)
# 使用tidyr包的fill()函数进行前向/后向填补
library(tidyr)
# 前向填补:用前一个非缺失值填补
fill(time_data, value, .direction = "down")
## fill: changed 4 values (40%) of 'value' (4 fewer NAs)
## day value
## 1 1 10
## 2 2 10
## 3 3 10
## 4 4 40
## 5 5 50
## 6 6 50
## 7 7 70
## 8 8 80
## 9 9 80
## 10 10 100
# 后向填补:用后一个非缺失值填补
fill(time_data, value, .direction = "up")
## fill: changed 4 values (40%) of 'value' (4 fewer NAs)
## day value
## 1 1 10
## 2 2 40
## 3 3 40
## 4 4 40
## 5 5 50
## 6 6 70
## 7 7 70
## 8 8 80
## 9 9 100
## 10 10 100
# 双向填补:先向后再向前(或相反)
fill(time_data, value, .direction = "downup")
## fill: changed 4 values (40%) of 'value' (4 fewer NAs)
## day value
## 1 1 10
## 2 2 10
## 3 3 10
## 4 4 40
## 5 5 50
## 6 6 50
## 7 7 70
## 8 8 80
## 9 9 80
## 10 10 100
小结:fill()函数简单易用,特别适合时间序列数据的缺失值填补。
对于连续型数据,线性插值是一种更精确的填补方法。
# 使用approx()函数进行线性插值
# 创建一个包含缺失值的向量
x <- 1:10
y <- c(1, NA, NA, 4, 5, NA, 7, 8, NA, 10)
# approx()进行线性插值
# x参数是自变量,y参数是因变量
interp_result <- approx(x = x[!is.na(y)], # 非缺失位置的x
y = y[!is.na(y)], # 非缺失位置的y
xout = x) # 需要插值的位置
# 查看插值结果
interp_result$y
## [1] 1 2 3 4 5 6 7 8 9 10
# zoo包提供了更便捷的插值函数
# 如果没有安装,先运行:install.packages("zoo")
library(zoo)
# na.approx()进行线性插值
na.approx(y)
## [1] 1 2 3 4 5 6 7 8 9 10
# na.spline()进行样条插值(更平滑)
na.spline(y)
## [1] 1 2 3 4 5 6 7 8 9 10
小结:线性插值假设数据点之间呈线性关系,适合变化趋势平稳的数据。
当变量之间存在相关性时,可以用其他变量预测缺失值。
# mice包(Multivariate Imputation by Chained Equations)
# 是最常用的多重插值包
# 如果没有安装,先运行:install.packages("mice")
library(mice)
# 创建示例数据
model_data <- data.frame(
x1 = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
x2 = c(2, 4, NA, 8, 10, 12, NA, 16, 18, 20),
x3 = c(3, 6, 9, NA, 15, 18, 21, NA, 27, 30)
)
# 查看缺失模式
md.pattern(model_data)
# 使用mice进行多重插值
# m = 5 表示生成5个插值数据集
# method = "pmm" 表示预测均值匹配法
imp <- mice(model_data, m = 5, method = "pmm", seed = 123)
# 查看插值结果
imp$imp$x2 # x2变量的5次插值结果
# 获取完整的插值数据集(取第一个)
complete_data <- complete(imp, 1)
complete_data
小结:mice包提供了多种插值方法,适合复杂数据的缺失值处理。
K近邻填补使用相似样本的值来填补缺失值。
# VIM包提供了kNN()函数
library(VIM)
# 创建示例数据
knn_data <- data.frame(
a = c(1, 2, NA, 4, 5, 6, 7, 8),
b = c(2, 4, 6, 8, 10, NA, 14, 16),
c = c(3, 6, 9, 12, 15, 18, NA, 24)
)
# kNN填补
# k = 5 表示使用5个最近邻
knn_result <- kNN(knn_data, k = 5)
# 查看填补结果
knn_result
小结:KNN填补基于相似性原理,适合样本之间有相似性的数据。
有时缺失本身可能包含信息,可以创建缺失指示变量。
# 创建缺失指示变量
sales_data$price_missing <- as.integer(is.na(sales_data$price))
sales_data$quantity_missing <- as.integer(is.na(sales_data$quantity))
# 查看结果
sales_data
## product price quantity revenue price_missing quantity_missing
## 1 A 100 10 1000 0 0
## 2 B NA 20 NA 1 0
## 3 C 200 NA NA 0 1
## 4 D 150 15 2250 0 0
## 5 E NA 25 3000 1 0
## 6 F 180 NA NA 0 1
## 7 G 220 30 6600 0 0
## 8 H NA 12 NA 1 0
# 使用dplyr批量创建缺失指示变量
library(dplyr)
sales_data %>%
mutate(
across(everything(),
~ as.integer(is.na(.)),
.names = "{.col}_missing")
)
## mutate: new variable 'product_missing' (integer) with one unique value and 0% NA
## new variable 'revenue_missing' (integer) with 2 unique values and 0% NA
## new variable 'price_missing_missing' (integer) with one unique value and 0% NA
## new variable 'quantity_missing_missing' (integer) with one unique value and 0% NA
## product price quantity revenue price_missing quantity_missing product_missing
## 1 A 100 10 1000 0 0 0
## 2 B NA 20 NA 1 0 0
## 3 C 200 NA NA 0 1 0
## 4 D 150 15 2250 0 0 0
## 5 E NA 25 3000 1 0 0
## 6 F 180 NA NA 0 1 0
## 7 G 220 30 6600 0 0 0
## 8 H NA 12 NA 1 0 0
## revenue_missing price_missing_missing quantity_missing_missing
## 1 0 0 0
## 2 1 0 0
## 3 1 0 0
## 4 0 0 0
## 5 0 0 0
## 6 1 0 0
## 7 0 0 0
## 8 1 0 0
小结:缺失指示变量可以捕捉”缺失”这一信息,在某些分析中很有价值。
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 删除法 | 缺失比例小 | 简单直接 | 损失信息 |
| 均值/中位数填补 | 数值变量 | 保持样本量 | 降低方差 |
| 前向/后向填补 | 时间序列 | 保持时序性 | 不适合随机缺失 |
| 线性插值 | 连续变量 | 较精确 | 假设线性关系 |
| 预测模型填补 | 变量相关 | 利用变量关系 | 计算复杂 |
| KNN填补 | 样本相似 | 利用样本相似性 | 需要定义距离 |
数据质量直接影响分析结果的可靠性。本章介绍如何识别和处理重复值与异常值。
重复数据会导致分析结果偏差,需要识别并处理。
# 创建包含重复行的数据
dup_data <- data.frame(
id = c(1, 2, 3, 2, 4, 3, 5),
name = c("A", "B", "C", "B", "D", "C", "E"),
value = c(10, 20, 30, 20, 40, 30, 50)
)
# duplicated()函数识别重复行
# 返回逻辑向量,TRUE表示该行是重复的
duplicated(dup_data)
## [1] FALSE FALSE FALSE TRUE FALSE TRUE FALSE
# 查看重复的行
dup_data[duplicated(dup_data), ]
## id name value
## 4 2 B 20
## 6 3 C 30
# 查看所有重复行(包括第一次出现的)
# 使用条件:该行出现次数大于1
dup_data[duplicated(dup_data) | duplicated(dup_data, fromLast = TRUE), ]
## id name value
## 2 2 B 20
## 3 3 C 30
## 4 2 B 20
## 6 3 C 30
# dplyr的distinct()识别唯一行
distinct(dup_data)
## distinct: removed 2 rows (29%), 5 rows remaining
## id name value
## 1 1 A 10
## 2 2 B 20
## 3 3 C 30
## 4 4 D 40
## 5 5 E 50
小结:duplicated()标记重复行,distinct()直接返回唯一行。
识别重复行后,可以选择删除。
# 使用unique()删除重复行
unique(dup_data)
## id name value
## 1 1 A 10
## 2 2 B 20
## 3 3 C 30
## 5 4 D 40
## 7 5 E 50
# 使用dplyr的distinct()删除重复行
distinct(dup_data)
## distinct: removed 2 rows (29%), 5 rows remaining
## id name value
## 1 1 A 10
## 2 2 B 20
## 3 3 C 30
## 4 4 D 40
## 5 5 E 50
# distinct()可以指定列来判断重复
# 只根据id和name判断重复
distinct(dup_data, id, name, .keep_all = TRUE)
## distinct: removed 2 rows (29%), 5 rows remaining
## id name value
## 1 1 A 10
## 2 2 B 20
## 3 3 C 30
## 4 4 D 40
## 5 5 E 50
小结:unique()简单直接,distinct()更灵活,可以指定判断重复的列。
有时只需要根据特定列判断重复。
# 创建数据:id相同但其他列不同
partial_dup <- data.frame(
id = c(1, 1, 2, 2, 3),
name = c("A", "A", "B", "B2", "C"),
value = c(10, 20, 30, 40, 50)
)
# 根据id去重,保留第一个出现的
distinct(partial_dup, id, .keep_all = TRUE)
## distinct: removed 2 rows (40%), 3 rows remaining
## id name value
## 1 1 A 10
## 2 2 B 30
## 3 3 C 50
# 根据id和name去重
distinct(partial_dup, id, name, .keep_all = TRUE)
## distinct: removed one row (20%), 4 rows remaining
## id name value
## 1 1 A 10
## 2 2 B 30
## 3 2 B2 40
## 4 3 C 50
# 使用duplicated()基于特定列
partial_dup[!duplicated(partial_dup$id), ]
## id name value
## 1 1 A 10
## 3 2 B 30
## 5 3 C 50
小结:基于特定列去重时,要注意保留哪一条记录。
异常值检测是数据分析的重要环节。
# 创建包含异常值的数据
outlier_data <- c(10, 12, 11, 13, 10, 100, 11, 12, -50, 14, 11, 13)
# 方法1:Z-score方法
# Z-score = (x - mean) / sd
# 通常|Z| > 3被认为是异常值
z_scores <- (outlier_data - mean(outlier_data)) / sd(outlier_data)
abs(z_scores) > 3 # 标记异常值
## [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# 查看被标记为异常值的点
outlier_data[abs(z_scores) > 3]
## numeric(0)
# 方法2:IQR方法(四分位距方法)
# IQR = Q3 - Q1
# 异常值定义:小于Q1-1.5*IQR 或 大于Q3+1.5*IQR
Q1 <- quantile(outlier_data, 0.25)
Q3 <- quantile(outlier_data, 0.75)
IQR_value <- Q3 - Q1
lower_bound <- Q1 - 1.5 * IQR_value
upper_bound <- Q3 + 1.5 * IQR_value
# 识别异常值
outlier_data < lower_bound | outlier_data > upper_bound
## [1] FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE TRUE FALSE FALSE FALSE
# 查看异常值
outlier_data[outlier_data < lower_bound | outlier_data > upper_bound]
## [1] 100 -50
小结:Z-score适合正态分布,IQR方法对非正态分布更稳健。
可视化是识别异常值的直观方法。
# 箱线图识别异常值
# 箱线图自动标记超出1.5*IQR范围的点
boxplot(outlier_data, main = "箱线图识别异常值")
# 获取箱线图识别的异常值
boxplot_stats <- boxplot.stats(outlier_data)
boxplot_stats$out # 异常值
## [1] 100 -50
# 散点图识别异常值
plot(outlier_data, main = "散点图识别异常值",
xlab = "索引", ylab = "值")
abline(h = upper_bound, col = "red", lty = 2) # 上边界
abline(h = lower_bound, col = "red", lty = 2) # 下边界
小结:箱线图是最常用的异常值可视化方法,直观且自动计算边界。
有时需要根据业务规则识别异常值。
# 创建年龄数据
age_data <- c(25, 30, 150, 28, -5, 35, 200, 40)
# 根据领域知识:年龄应该在0-120之间
# 定义合理范围
min_age <- 0
max_age <- 120
# 识别异常值
age_data < min_age | age_data > max_age
## [1] FALSE FALSE TRUE FALSE TRUE FALSE TRUE FALSE
# 查看异常值
age_data[age_data < min_age | age_data > max_age]
## [1] 150 -5 200
# 创建标记列
age_df <- data.frame(age = age_data)
age_df$is_outlier <- age_df$age < min_age | age_df$age > max_age
age_df
## age is_outlier
## 1 25 FALSE
## 2 30 FALSE
## 3 150 TRUE
## 4 28 FALSE
## 5 -5 TRUE
## 6 35 FALSE
## 7 200 TRUE
## 8 40 FALSE
小结:领域知识规则需要结合具体业务场景,是最实用的异常值识别方法。
截尾(Winsorize)将极端值替换为边界值,而不是删除。
# 手动实现Winsorize截尾处理
# 将最小的5%和最大的5%截尾
# 计算边界值
lower_bound <- quantile(outlier_data, 0.05)
upper_bound <- quantile(outlier_data, 0.95)
# 进行截尾处理
winsorize_result <- pmin(pmax(outlier_data, lower_bound), upper_bound)
# 比较原始数据和截尾后数据
data.frame(
original = outlier_data,
winsorized = winsorize_result
)
## original winsorized
## 1 10 10.0
## 2 12 12.0
## 3 11 11.0
## 4 13 13.0
## 5 10 10.0
## 6 100 52.7
## 7 11 11.0
## 8 12 12.0
## 9 -50 -17.0
## 10 14 14.0
## 11 11 11.0
## 12 13 13.0
小结:截尾保留了样本量,同时减少了极端值的影响。
数据转换可以减少异常值的影响。
# 创建右偏数据(有极端大值)
skewed_data <- c(1, 2, 3, 5, 8, 10, 15, 20, 50, 100)
# 对数转换
log_data <- log(skewed_data)
# 平方根转换
sqrt_data <- sqrt(skewed_data)
# 比较转换前后的分布
par(mfrow = c(1, 3)) # 设置1行3列的图形布局
hist(skewed_data, main = "原始数据", col = "lightblue")
hist(log_data, main = "对数转换", col = "lightgreen")
hist(sqrt_data, main = "平方根转换", col = "lightpink")
par(mfrow = c(1, 1)) # 恢复默认布局
小结:对数转换适合右偏数据,可以压缩大值的影响。
将异常值替换为上下边界值。
# 使用IQR方法计算边界
replace_data <- outlier_data
# 替换低于下界的值为下界
replace_data[replace_data < lower_bound] <- lower_bound
# 替换高于上界的值为上界
replace_data[replace_data > upper_bound] <- upper_bound
# 比较原始和处理后的数据
data.frame(
original = outlier_data,
replaced = replace_data
)
## original replaced
## 1 10 10.0
## 2 12 12.0
## 3 11 11.0
## 4 13 13.0
## 5 10 10.0
## 6 100 52.7
## 7 11 11.0
## 8 12 12.0
## 9 -50 -17.0
## 10 14 14.0
## 11 11 11.0
## 12 13 13.0
小结:替换为边界值保留了数据方向,同时限制了极端值。
当异常值确实是错误数据时,可以选择删除。
# 删除异常值
clean_data <- outlier_data[outlier_data >= lower_bound & outlier_data <= upper_bound]
# 比较删除前后的数据
cat("原始数据长度:", length(outlier_data), "\n")
## 原始数据长度: 12
cat("删除后数据长度:", length(clean_data), "\n")
## 删除后数据长度: 10
cat("删除的异常值:", setdiff(outlier_data, clean_data))
## 删除的异常值: 100 -50
小结:删除异常值要谨慎,确保删除的确实是错误数据而非真实极端值。
| 问题类型 | 检测方法 | 处理方法 |
|---|---|---|
| 重复行 | duplicated() |
unique()、distinct() |
| 异常值(统计) | Z-score、IQR | 截尾、替换、删除 |
| 异常值(可视化) | 箱线图、散点图 | 转换、替换 |
| 异常值(业务) | 领域规则 | 根据业务决定 |
正确的数据类型是数据分析的基础。本章介绍各种数据类型转换和变量重编码方法。
R中常见的数据类型包括数值型、字符型、因子型等。
# 创建示例数据
type_data <- data.frame(
id = 1:5,
age = c("25", "30", "28", "35", "40"), # 字符型年龄
score = c("85.5", "90.0", "78.5", "92.0", "88.0"), # 字符型分数
gender = c("M", "F", "M", "F", "M"), # 字符型性别
stringsAsFactors = FALSE
)
# 查看当前类型
str(type_data)
## 'data.frame': 5 obs. of 4 variables:
## $ id : int 1 2 3 4 5
## $ age : chr "25" "30" "28" "35" ...
## $ score : chr "85.5" "90.0" "78.5" "92.0" ...
## $ gender: chr "M" "F" "M" "F" ...
# as.numeric()转换为数值型
type_data$age_num <- as.numeric(type_data$age)
type_data$score_num <- as.numeric(type_data$score)
# as.character()转换为字符型
num_vector <- c(1, 2, 3, 4, 5)
char_vector <- as.character(num_vector)
char_vector
## [1] "1" "2" "3" "4" "5"
# as.factor()转换为因子型
type_data$gender_factor <- as.factor(type_data$gender)
levels(type_data$gender_factor) # 查看因子水平
## [1] "F" "M"
# as.logical()转换为逻辑型
num_to_logic <- c(0, 1, 2, 0, 1)
as.logical(num_to_logic) # 0为FALSE,非0为TRUE
## [1] FALSE TRUE TRUE FALSE TRUE
# as.Date()转换为日期型
date_string <- c("2024-01-01", "2024-06-15", "2024-12-31")
date_vector <- as.Date(date_string)
date_vector
## [1] "2024-01-01" "2024-06-15" "2024-12-31"
小结:as.*()系列函数是类型转换的基础,注意转换可能产生NA。
因子变量需要正确设置水平和标签。
# 创建因子变量
gender_factor <- factor(c("M", "F", "M", "F", "M"))
# 查看当前水平
levels(gender_factor)
## [1] "F" "M"
# 使用levels()重命名水平
levels(gender_factor) <- c("Female", "Male")
gender_factor
## [1] Male Female Male Female Male
## Levels: Female Male
# 使用forcats包进行因子操作
# 如果没有安装,先运行:install.packages("forcats")
library(forcats)
# fct_recode()重新编码水平
gender_new <- fct_recode(gender_factor,
"女性" = "Female",
"男性" = "Male")
levels(gender_new)
## [1] "女性" "男性"
# fct_relevel()调整水平顺序
# 将"男性"设为第一个水平
fct_relevel(gender_new, "男性")
## [1] 男性 女性 男性 女性 男性
## Levels: 男性 女性
# fct_infreq()按频率排序水平
city_data <- factor(c("北京", "上海", "北京", "广州", "北京", "上海"))
fct_infreq(city_data) # 出现频率高的排前面
## [1] 北京 上海 北京 广州 北京 上海
## Levels: 北京 上海 广州
小结:forcats包提供了丰富的因子操作函数,推荐使用。
将连续变量转换为分类变量。
# 创建连续变量
continuous_data <- c(18, 22, 35, 45, 55, 65, 75, 85)
# 使用cut()函数离散化
# breaks指定分割点
age_group <- cut(continuous_data,
breaks = c(0, 30, 50, 100), # 分割点
labels = c("青年", "中年", "老年"), # 标签
right = FALSE) # 左闭右开区间
age_group
## [1] 青年 青年 中年 中年 老年 老年 老年 老年
## Levels: 青年 中年 老年
# 使用Hmisc包的cut2()函数
# 可以按分位数分割
# 如果没有安装,先运行:install.packages("Hmisc")
library(Hmisc)
# 按分位数分成3组
age_group2 <- cut2(continuous_data, g = 3)
age_group2
## [1] [18,45) [18,45) [18,45) [45,75) [45,75) [45,75) [75,85] [75,85]
## Levels: [18,45) [45,75) [75,85]
小结:cut()需要手动指定分割点,cut2()可以自动按分位数分割。
标准化使不同量纲的变量可比。
# 创建示例数据
scale_data <- data.frame(
height = c(160, 165, 170, 175, 180), # 身高(cm)
weight = c(50, 55, 60, 65, 70) # 体重(kg)
)
# scale()函数进行Z-score标准化
# 标准化后均值为0,标准差为1
scale_result <- scale(scale_data$height)
scale_result
## [,1]
## [1,] -1.2649111
## [2,] -0.6324555
## [3,] 0.0000000
## [4,] 0.6324555
## [5,] 1.2649111
## attr(,"scaled:center")
## [1] 170
## attr(,"scaled:scale")
## [1] 7.905694
# 对整个数据框标准化
scale_all <- as.data.frame(scale(scale_data))
scale_all
## height weight
## 1 -1.2649111 -1.2649111
## 2 -0.6324555 -0.6324555
## 3 0.0000000 0.0000000
## 4 0.6324555 0.6324555
## 5 1.2649111 1.2649111
# 使用caret包进行预处理
# 如果没有安装,先运行:install.packages("caret")
library(caret)
# 创建预处理对象
preproc <- preProcess(scale_data, method = c("center", "scale"))
# 应用预处理
predict(preproc, scale_data)
## height weight
## 1 -1.2649111 -1.2649111
## 2 -0.6324555 -0.6324555
## 3 0.0000000 0.0000000
## 4 0.6324555 0.6324555
## 5 1.2649111 1.2649111
# 归一化(缩放到0-1范围)
preproc_range <- preProcess(scale_data, method = "range")
predict(preproc_range, scale_data)
## height weight
## 1 0.00 0.00
## 2 0.25 0.25
## 3 0.50 0.50
## 4 0.75 0.75
## 5 1.00 1.00
小结:scale()简单直接,caret::preProcess()提供更多预处理选项。
将分类变量转换为数值型虚拟变量。
# 创建示例数据
dummy_data <- data.frame(
id = 1:6,
color = c("red", "blue", "green", "red", "blue", "green"),
size = c("S", "M", "L", "M", "L", "S")
)
# 使用model.matrix()创建虚拟变量
# 第一个参数是公式,~ 表示用所有变量
dummy_matrix <- model.matrix(~ color + size, data = dummy_data)
dummy_matrix
## (Intercept) colorgreen colorred sizeM sizeS
## 1 1 0 1 0 1
## 2 1 0 0 1 0
## 3 1 1 0 0 0
## 4 1 0 1 1 0
## 5 1 0 0 0 0
## 6 1 1 0 0 1
## attr(,"assign")
## [1] 0 1 1 2 2
## attr(,"contrasts")
## attr(,"contrasts")$color
## [1] "contr.treatment"
##
## attr(,"contrasts")$size
## [1] "contr.treatment"
# 使用fastDummies包
# 如果没有安装,先运行:install.packages("fastDummies")
library(fastDummies)
# dummy_cols()创建虚拟变量列
dummy_cols(dummy_data, select_columns = c("color", "size"))
## id color size color_blue color_green color_red size_L size_M size_S
## 1 1 red S 0 0 1 0 0 1
## 2 2 blue M 1 0 0 0 1 0
## 3 3 green L 0 1 0 1 0 0
## 4 4 red M 0 0 1 0 1 0
## 5 5 blue L 1 0 0 1 0 0
## 6 6 green S 0 1 0 0 0 1
小结:model.matrix()是R内置方法,fastDummies包更直观易用。
修改变量名称使其更清晰。
# 创建示例数据
rename_data <- data.frame(
a = 1:5,
b = c("A", "B", "C", "D", "E"),
c = c(10, 20, 30, 40, 50)
)
# 使用dplyr的rename()函数
rename_data <- rename(rename_data,
id = a, # 新名 = 旧名
name = b,
value = c)
## rename: renamed 3 variables (id, name, value)
rename_data
## id name value
## 1 1 A 10
## 2 2 B 20
## 3 3 C 30
## 4 4 D 40
## 5 5 E 50
# 使用data.table的setnames()函数
library(data.table)
setnames(rename_data, old = c("id", "name", "value"),
new = c("编号", "名称", "数值"))
rename_data
## 编号 名称 数值
## 1 1 A 10
## 2 2 B 20
## 3 3 C 30
## 4 4 D 40
## 5 5 E 50
小结:rename()语法清晰,setnames()可以直接修改原数据。
调整变量的顺序。
# 创建示例数据
order_data <- data.frame(
z = 1:5,
a = letters[1:5],
m = rnorm(5)
)
# 使用dplyr的select()调整顺序
# 按字母顺序排列
select(order_data, sort(names(order_data)))
## select: columns reordered (a, m, z)
## a m z
## 1 a 0.04389663 1
## 2 b -0.47753100 2
## 3 c 1.22973494 3
## 4 d -0.83978589 4
## 5 e -0.88992089 5
# 手动指定顺序
select(order_data, a, m, z)
## select: columns reordered (a, m, z)
## a m z
## 1 a 0.04389663 1
## 2 b -0.47753100 2
## 3 c 1.22973494 3
## 4 d -0.83978589 4
## 5 e -0.88992089 5
# 将某列移到最前面
select(order_data, z, everything())
## select: no changes
## z a m
## 1 1 a 0.04389663
## 2 2 b -0.47753100
## 3 3 c 1.22973494
## 4 4 d -0.83978589
## 5 5 e -0.88992089
# 使用relocate()函数(dplyr新版本)
relocate(order_data, a, .before = z)
## relocate: columns reordered (a, z, m)
## a z m
## 1 a 1 0.04389663
## 2 b 2 -0.47753100
## 3 c 3 1.22973494
## 4 d 4 -0.83978589
## 5 e 5 -0.88992089
小结:select()和relocate()都可以调整变量顺序。
数学变换可以改变数据分布。
# 创建示例数据
transform_data <- data.frame(
x = c(1, 10, 100, 1000, 10000)
)
# 对数变换
transform_data$log_x <- log(transform_data$x) # 自然对数
transform_data$log10_x <- log10(transform_data$x) # 以10为底的对数
transform_data$log2_x <- log2(transform_data$x) # 以2为底的对数
# 平方根变换
transform_data$sqrt_x <- sqrt(transform_data$x)
# 平方变换
transform_data$square_x <- transform_data$x^2
# 倒数变换
transform_data$reciprocal_x <- 1 / transform_data$x
# 查看结果
transform_data
## x log_x log10_x log2_x sqrt_x square_x reciprocal_x
## 1 1 0.000000 0 0.000000 1.000000 1e+00 1e+00
## 2 10 2.302585 1 3.321928 3.162278 1e+02 1e-01
## 3 100 4.605170 2 6.643856 10.000000 1e+04 1e-02
## 4 1000 6.907755 3 9.965784 31.622777 1e+06 1e-03
## 5 10000 9.210340 4 13.287712 100.000000 1e+08 1e-04
小结:对数变换适合右偏数据,平方根变换适合计数数据。
交互项用于建模中捕捉变量间的交互作用。
# 创建示例数据
interact_data <- data.frame(
x = 1:5,
y = c(2, 4, 6, 8, 10)
)
# 创建交互项(乘积)
interact_data$xy <- interact_data$x * interact_data$y
# 使用mutate()创建
library(dplyr)
interact_data <- interact_data %>%
mutate(xy_interaction = x * y)
## mutate: new variable 'xy_interaction' (double) with 5 unique values and 0% NA
# 在模型中自动创建交互项
# lm(y ~ x1 * x2, data) 会自动包含x1、x2和它们的交互项
小结:交互项在回归分析中很重要,可以手动创建或让模型自动创建。
将连续变量分成若干组。
# 创建示例数据
bin_data <- data.frame(
score = c(45, 55, 65, 75, 85, 95, 100, 35, 72, 88)
)
# 使用cut()分箱
bin_data$grade <- cut(bin_data$score,
breaks = c(0, 60, 70, 80, 90, 100),
labels = c("F", "D", "C", "B", "A"),
include.lowest = TRUE)
bin_data
## score grade
## 1 45 F
## 2 55 F
## 3 65 D
## 4 75 C
## 5 85 B
## 6 95 A
## 7 100 A
## 8 35 F
## 9 72 C
## 10 88 B
# 等宽分箱(每箱范围相等)
bin_data$equal_width <- cut(bin_data$score,
breaks = 5, # 分成5箱
labels = c("E", "D", "C", "B", "A"))
bin_data
## score grade equal_width
## 1 45 F E
## 2 55 F D
## 3 65 D C
## 4 75 C B
## 5 85 B B
## 6 95 A A
## 7 100 A A
## 8 35 F E
## 9 72 C C
## 10 88 B A
# 等频分箱(每箱数量相等)
# 使用quantile()计算分位数
breaks <- quantile(bin_data$score, probs = seq(0, 1, 0.2))
bin_data$equal_freq <- cut(bin_data$score,
breaks = breaks,
include.lowest = TRUE)
bin_data
## score grade equal_width equal_freq
## 1 45 F E [35,53]
## 2 55 F D (53,69.2]
## 3 65 D C (53,69.2]
## 4 75 C B (69.2,79]
## 5 85 B B (79,89.4]
## 6 95 A A (89.4,100]
## 7 100 A A (89.4,100]
## 8 35 F E [35,53]
## 9 72 C C (69.2,79]
## 10 88 B A (79,89.4]
小结:分箱策略有等宽、等频和自定义,根据数据特点选择。
| 操作 | 函数 | 说明 |
|---|---|---|
| 类型转换 | as.numeric()等 |
转换数据类型 |
| 因子重编码 | fct_recode() |
修改因子水平 |
| 离散化 | cut()、cut2() |
连续转分类 |
| 标准化 | scale() |
Z-score标准化 |
| 虚拟变量 | dummy_cols() |
分类转数值 |
| 重命名 | rename() |
修改变量名 |
| 重排序 | select()、relocate() |
调整变量顺序 |
| 变换 | log()、sqrt() |
数学变换 |
数据筛选和排序是数据处理中最常用的操作。本章介绍各种筛选和排序方法。
根据条件选择满足要求的行。
# 创建示例数据
filter_data <- data.frame(
id = 1:10,
name = c("张三", "李四", "王五", "赵六", "钱七",
"孙八", "周九", "吴十", "郑一", "陈二"),
age = c(25, 30, 28, 35, 40, 22, 45, 33, 29, 31),
department = c("销售", "技术", "销售", "技术", "财务",
"销售", "技术", "财务", "销售", "技术"),
salary = c(5000, 8000, 5500, 9000, 7000, 4500, 10000, 7500, 5200, 8500)
)
# 使用dplyr的filter()筛选
library(dplyr)
# 筛选年龄大于30的员工
filter(filter_data, age > 30)
## filter: removed 5 rows (50%), 5 rows remaining
## id name age department salary
## 1 4 赵六 35 技术 9000
## 2 5 钱七 40 财务 7000
## 3 7 周九 45 技术 10000
## 4 8 吴十 33 财务 7500
## 5 10 陈二 31 技术 8500
# 筛选技术部门的员工
filter(filter_data, department == "技术")
## filter: removed 6 rows (60%), 4 rows remaining
## id name age department salary
## 1 2 李四 30 技术 8000
## 2 4 赵六 35 技术 9000
## 3 7 周九 45 技术 10000
## 4 10 陈二 31 技术 8500
# 筛选年龄大于30且薪资大于8000的员工
filter(filter_data, age > 30 & salary > 8000)
## filter: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 4 赵六 35 技术 9000
## 2 7 周九 45 技术 10000
## 3 10 陈二 31 技术 8500
# 使用base R的subset()函数
subset(filter_data, age > 30)
## id name age department salary
## 4 4 赵六 35 技术 9000
## 5 5 钱七 40 财务 7000
## 7 7 周九 45 技术 10000
## 8 8 吴十 33 财务 7500
## 10 10 陈二 31 技术 8500
小结:filter()语法简洁,subset()是R内置函数,两者功能相似。
选择需要的变量列。
# 使用dplyr的select()选择列
# 按名称选择
select(filter_data, name, age)
## select: dropped 3 variables (id, department, salary)
## name age
## 1 张三 25
## 2 李四 30
## 3 王五 28
## 4 赵六 35
## 5 钱七 40
## 6 孙八 22
## 7 周九 45
## 8 吴十 33
## 9 郑一 29
## 10 陈二 31
# 选择连续的列
select(filter_data, id:age)
## select: dropped 2 variables (department, salary)
## id name age
## 1 1 张三 25
## 2 2 李四 30
## 3 3 王五 28
## 4 4 赵六 35
## 5 5 钱七 40
## 6 6 孙八 22
## 7 7 周九 45
## 8 8 吴十 33
## 9 9 郑一 29
## 10 10 陈二 31
# 排除某些列
select(filter_data, -id, -name)
## select: dropped 2 variables (id, name)
## age department salary
## 1 25 销售 5000
## 2 30 技术 8000
## 3 28 销售 5500
## 4 35 技术 9000
## 5 40 财务 7000
## 6 22 销售 4500
## 7 45 技术 10000
## 8 33 财务 7500
## 9 29 销售 5200
## 10 31 技术 8500
# 按位置选择
select(filter_data, 2, 3)
## select: dropped 3 variables (id, department, salary)
## name age
## 1 张三 25
## 2 李四 30
## 3 王五 28
## 4 赵六 35
## 5 钱七 40
## 6 孙八 22
## 7 周九 45
## 8 吴十 33
## 9 郑一 29
## 10 陈二 31
# 按条件选择(包含"e"的列名)
select(filter_data, contains("e"))
## select: dropped 2 variables (id, salary)
## name age department
## 1 张三 25 销售
## 2 李四 30 技术
## 3 王五 28 销售
## 4 赵六 35 技术
## 5 钱七 40 财务
## 6 孙八 22 销售
## 7 周九 45 技术
## 8 吴十 33 财务
## 9 郑一 29 销售
## 10 陈二 31 技术
# 按正则表达式选择
select(filter_data, matches("^[a-z]{4}$"))
## select: dropped 4 variables (id, age, department, salary)
## name
## 1 张三
## 2 李四
## 3 王五
## 4 赵六
## 5 钱七
## 6 孙八
## 7 周九
## 8 吴十
## 9 郑一
## 10 陈二
# 选择特定类型的列
select(filter_data, where(is.numeric)) # 选择数值列
## select: dropped 2 variables (name, department)
## id age salary
## 1 1 25 5000
## 2 2 30 8000
## 3 3 28 5500
## 4 4 35 9000
## 5 5 40 7000
## 6 6 22 4500
## 7 7 45 10000
## 8 8 33 7500
## 9 9 29 5200
## 10 10 31 8500
select(filter_data, where(is.character)) # 选择字符列
## select: dropped 3 variables (id, age, salary)
## name department
## 1 张三 销售
## 2 李四 技术
## 3 王五 销售
## 4 赵六 技术
## 5 钱七 财务
## 6 孙八 销售
## 7 周九 技术
## 8 吴十 财务
## 9 郑一 销售
## 10 陈二 技术
小结:select()支持多种选择方式,灵活强大。
筛选唯一值。
# 选择部门列的唯一值
distinct(filter_data, department)
## distinct: removed 7 rows (70%), 3 rows remaining
## department
## 1 销售
## 2 技术
## 3 财务
# 选择部门和年龄的唯一组合
distinct(filter_data, department, age)
## distinct: no rows removed
## department age
## 1 销售 25
## 2 技术 30
## 3 销售 28
## 4 技术 35
## 5 财务 40
## 6 销售 22
## 7 技术 45
## 8 财务 33
## 9 销售 29
## 10 技术 31
# 保留所有列,但只保留唯一行
distinct(filter_data, department, .keep_all = TRUE)
## distinct: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 2 李四 30 技术 8000
## 3 5 钱七 40 财务 7000
小结:distinct()可以快速获取唯一值或唯一组合。
从数据中随机抽取样本。
# 设置随机种子,保证结果可重复
set.seed(123)
# sample_n()随机抽取n行
sample_n(filter_data, 3) # 随机抽取3行
## sample_n: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 3 王五 28 销售 5500
## 2 10 陈二 31 技术 8500
## 3 2 李四 30 技术 8000
# sample_frac()随机抽取一定比例
sample_frac(filter_data, 0.3) # 随机抽取30%
## sample_frac: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 2 李四 30 技术 8000
## 2 6 孙八 22 销售 4500
## 3 3 王五 28 销售 5500
# slice_sample()是新版推荐函数
slice_sample(filter_data, n = 3) # 抽取3行
## slice_sample: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 5 钱七 40 财务 7000
## 2 4 赵六 35 技术 9000
## 3 6 孙八 22 销售 4500
slice_sample(filter_data, prop = 0.3) # 抽取30%
## slice_sample: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 6 孙八 22 销售 4500
## 2 9 郑一 29 销售 5200
## 3 2 李四 30 技术 8000
小结:slice_sample()是最新推荐的随机抽样函数。
对数据进行排序。
# 使用dplyr的arrange()排序
# 按年龄升序排列
arrange(filter_data, age)
## id name age department salary
## 1 6 孙八 22 销售 4500
## 2 1 张三 25 销售 5000
## 3 3 王五 28 销售 5500
## 4 9 郑一 29 销售 5200
## 5 2 李四 30 技术 8000
## 6 10 陈二 31 技术 8500
## 7 8 吴十 33 财务 7500
## 8 4 赵六 35 技术 9000
## 9 5 钱七 40 财务 7000
## 10 7 周九 45 技术 10000
# 按年龄降序排列
arrange(filter_data, desc(age))
## id name age department salary
## 1 7 周九 45 技术 10000
## 2 5 钱七 40 财务 7000
## 3 4 赵六 35 技术 9000
## 4 8 吴十 33 财务 7500
## 5 10 陈二 31 技术 8500
## 6 2 李四 30 技术 8000
## 7 9 郑一 29 销售 5200
## 8 3 王五 28 销售 5500
## 9 1 张三 25 销售 5000
## 10 6 孙八 22 销售 4500
# 按多个变量排序
arrange(filter_data, department, desc(salary))
## id name age department salary
## 1 7 周九 45 技术 10000
## 2 4 赵六 35 技术 9000
## 3 10 陈二 31 技术 8500
## 4 2 李四 30 技术 8000
## 5 8 吴十 33 财务 7500
## 6 5 钱七 40 财务 7000
## 7 3 王五 28 销售 5500
## 8 9 郑一 29 销售 5200
## 9 1 张三 25 销售 5000
## 10 6 孙八 22 销售 4500
# 使用base R的order()函数
filter_data[order(filter_data$age), ] # 升序
## id name age department salary
## 6 6 孙八 22 销售 4500
## 1 1 张三 25 销售 5000
## 3 3 王五 28 销售 5500
## 9 9 郑一 29 销售 5200
## 2 2 李四 30 技术 8000
## 10 10 陈二 31 技术 8500
## 8 8 吴十 33 财务 7500
## 4 4 赵六 35 技术 9000
## 5 5 钱七 40 财务 7000
## 7 7 周九 45 技术 10000
filter_data[order(filter_data$age, decreasing = TRUE), ] # 降序
## id name age department salary
## 7 7 周九 45 技术 10000
## 5 5 钱七 40 财务 7000
## 4 4 赵六 35 技术 9000
## 8 8 吴十 33 财务 7500
## 10 10 陈二 31 技术 8500
## 2 2 李四 30 技术 8000
## 9 9 郑一 29 销售 5200
## 3 3 王五 28 销售 5500
## 1 1 张三 25 销售 5000
## 6 6 孙八 22 销售 4500
# 使用sort()对向量排序
sort(filter_data$age)
## [1] 22 25 28 29 30 31 33 35 40 45
sort(filter_data$age, decreasing = TRUE)
## [1] 45 40 35 33 31 30 29 28 25 22
小结:arrange()语法简洁,order()是R内置函数。
分组后在各组内排序。
# 按部门分组,在每个部门内按薪资降序排列
filter_data %>%
group_by(department) %>%
arrange(desc(salary), .by_group = TRUE)
## group_by: one grouping variable (department)
## # A tibble: 10 × 5
## # Groups: department [3]
## id name age department salary
## <int> <chr> <dbl> <chr> <dbl>
## 1 7 周九 45 技术 10000
## 2 4 赵六 35 技术 9000
## 3 10 陈二 31 技术 8500
## 4 2 李四 30 技术 8000
## 5 8 吴十 33 财务 7500
## 6 5 钱七 40 财务 7000
## 7 3 王五 28 销售 5500
## 8 9 郑一 29 销售 5200
## 9 1 张三 25 销售 5000
## 10 6 孙八 22 销售 4500
# 或者使用arrange()配合group_by()
filter_data %>%
group_by(department) %>%
arrange(department, desc(salary))
## group_by: one grouping variable (department)
## # A tibble: 10 × 5
## # Groups: department [3]
## id name age department salary
## <int> <chr> <dbl> <chr> <dbl>
## 1 7 周九 45 技术 10000
## 2 4 赵六 35 技术 9000
## 3 10 陈二 31 技术 8500
## 4 2 李四 30 技术 8000
## 5 8 吴十 33 财务 7500
## 6 5 钱七 40 财务 7000
## 7 3 王五 28 销售 5500
## 8 9 郑一 29 销售 5200
## 9 1 张三 25 销售 5000
## 10 6 孙八 22 销售 4500
小结:分组排序常用于组内排名等场景。
使用各种条件进行筛选。
# 与条件(AND)
filter(filter_data, age > 30, salary > 7000) # 逗号分隔表示AND
## filter: removed 6 rows (60%), 4 rows remaining
## id name age department salary
## 1 4 赵六 35 技术 9000
## 2 7 周九 45 技术 10000
## 3 8 吴十 33 财务 7500
## 4 10 陈二 31 技术 8500
filter(filter_data, age > 30 & salary > 7000) # 或使用&
## filter: removed 6 rows (60%), 4 rows remaining
## id name age department salary
## 1 4 赵六 35 技术 9000
## 2 7 周九 45 技术 10000
## 3 8 吴十 33 财务 7500
## 4 10 陈二 31 技术 8500
# 或条件(OR)
filter(filter_data, department == "销售" | department == "技术")
## filter: removed 2 rows (20%), 8 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 2 李四 30 技术 8000
## 3 3 王五 28 销售 5500
## 4 4 赵六 35 技术 9000
## 5 6 孙八 22 销售 4500
## 6 7 周九 45 技术 10000
## 7 9 郑一 29 销售 5200
## 8 10 陈二 31 技术 8500
# 非条件(NOT)
filter(filter_data, !(department == "财务"))
## filter: removed 2 rows (20%), 8 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 2 李四 30 技术 8000
## 3 3 王五 28 销售 5500
## 4 4 赵六 35 技术 9000
## 5 6 孙八 22 销售 4500
## 6 7 周九 45 技术 10000
## 7 9 郑一 29 销售 5200
## 8 10 陈二 31 技术 8500
# %in%运算符:判断值是否在集合中
filter(filter_data, department %in% c("销售", "技术"))
## filter: removed 2 rows (20%), 8 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 2 李四 30 技术 8000
## 3 3 王五 28 销售 5500
## 4 4 赵六 35 技术 9000
## 5 6 孙八 22 销售 4500
## 6 7 周九 45 技术 10000
## 7 9 郑一 29 销售 5200
## 8 10 陈二 31 技术 8500
# between()函数:判断值是否在范围内
filter(filter_data, between(age, 25, 35))
## filter: removed 3 rows (30%), 7 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 2 李四 30 技术 8000
## 3 3 王五 28 销售 5500
## 4 4 赵六 35 技术 9000
## 5 8 吴十 33 财务 7500
## 6 9 郑一 29 销售 5200
## 7 10 陈二 31 技术 8500
# 判断是否为NA
filter(filter_data, !is.na(age))
## filter: no rows removed
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 2 李四 30 技术 8000
## 3 3 王五 28 销售 5500
## 4 4 赵六 35 技术 9000
## 5 5 钱七 40 财务 7000
## 6 6 孙八 22 销售 4500
## 7 7 周九 45 技术 10000
## 8 8 吴十 33 财务 7500
## 9 9 郑一 29 销售 5200
## 10 10 陈二 31 技术 8500
小结:%in%是常用的集合判断运算符,简洁高效。
筛选没有缺失值的行。
# 创建含缺失值的数据
na_filter_data <- filter_data
na_filter_data$salary[c(2, 5, 8)] <- NA
# 筛选salary非缺失的行
filter(na_filter_data, !is.na(salary))
## filter: removed 3 rows (30%), 7 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 3 王五 28 销售 5500
## 3 4 赵六 35 技术 9000
## 4 6 孙八 22 销售 4500
## 5 7 周九 45 技术 10000
## 6 9 郑一 29 销售 5200
## 7 10 陈二 31 技术 8500
# 筛选所有列都非缺失的行(使用基础R方法)
na_filter_data[complete.cases(na_filter_data), ]
## id name age department salary
## 1 1 张三 25 销售 5000
## 3 3 王五 28 销售 5500
## 4 4 赵六 35 技术 9000
## 6 6 孙八 22 销售 4500
## 7 7 周九 45 技术 10000
## 9 9 郑一 29 销售 5200
## 10 10 陈二 31 技术 8500
# 使用drop_na()
drop_na(na_filter_data)
## drop_na: removed 3 rows (30%), 7 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 3 王五 28 销售 5500
## 3 4 赵六 35 技术 9000
## 4 6 孙八 22 销售 4500
## 5 7 周九 45 技术 10000
## 6 9 郑一 29 销售 5200
## 7 10 陈二 31 技术 8500
drop_na(na_filter_data, salary) # 只考虑salary列
## drop_na: removed 3 rows (30%), 7 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 3 王五 28 销售 5500
## 3 4 赵六 35 技术 9000
## 4 6 孙八 22 销售 4500
## 5 7 周九 45 技术 10000
## 6 9 郑一 29 销售 5200
## 7 10 陈二 31 技术 8500
小结:drop_na()是筛选非缺失值的便捷方法。
根据行号选择数据。
# 使用slice()按行号选择
# 选择第1到3行
slice(filter_data, 1:3)
## slice: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 2 李四 30 技术 8000
## 3 3 王五 28 销售 5500
# 选择第1、3、5行
slice(filter_data, c(1, 3, 5))
## slice: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 3 王五 28 销售 5500
## 3 5 钱七 40 财务 7000
# 选择最后一行
slice_tail(filter_data, n = 1)
## slice_tail: removed 9 rows (90%), one row remaining
## id name age department salary
## 1 10 陈二 31 技术 8500
# 选择前3行
slice_head(filter_data, n = 3)
## slice_head: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 1 张三 25 销售 5000
## 2 2 李四 30 技术 8000
## 3 3 王五 28 销售 5500
# 选择最大/最小的行
slice_max(filter_data, salary, n = 3) # 薪资最高的3人
## slice_max: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 7 周九 45 技术 10000
## 2 4 赵六 35 技术 9000
## 3 10 陈二 31 技术 8500
slice_min(filter_data, age, n = 3) # 年龄最小的3人
## slice_min: removed 7 rows (70%), 3 rows remaining
## id name age department salary
## 1 6 孙八 22 销售 4500
## 2 1 张三 25 销售 5000
## 3 3 王五 28 销售 5500
小结:slice()系列函数提供了灵活的行选择方式。
| 操作 | 函数 | 说明 |
|---|---|---|
| 行筛选 | filter() |
按条件筛选行 |
| 列筛选 | select() |
选择列 |
| 去重 | distinct() |
唯一值 |
| 随机抽样 | slice_sample() |
随机抽取 |
| 排序 | arrange() |
排序 |
| 行号筛选 | slice() |
按位置选择 |
数据变换是创建新变量和进行计算的核心操作。本章介绍各种数据变换方法。
在数据框中创建新变量。
# 创建示例数据
transform_data <- data.frame(
product = c("A", "B", "C", "D", "E"),
price = c(100, 200, 150, 300, 250),
quantity = c(10, 5, 8, 3, 6)
)
# 使用mutate()添加新列
library(dplyr)
transform_data <- transform_data %>%
mutate(
revenue = price * quantity, # 收入
unit_cost = price * 0.6, # 单位成本
profit = revenue - unit_cost * quantity # 利润
)
## mutate: new variable 'revenue' (double) with 4 unique values and 0% NA
## new variable 'unit_cost' (double) with 5 unique values and 0% NA
## new variable 'profit' (double) with 4 unique values and 0% NA
transform_data
## product price quantity revenue unit_cost profit
## 1 A 100 10 1000 60 400
## 2 B 200 5 1000 120 400
## 3 C 150 8 1200 90 480
## 4 D 300 3 900 180 360
## 5 E 250 6 1500 150 600
# transmute()只保留新创建的列
transmute(transform_data,
product = product,
revenue = price * quantity)
## transmute: dropped 4 variables (price, quantity, unit_cost, profit)
## transmute: dropped 4 variables (price, quantity, unit_cost, profit)
## product revenue
## 1 A 1000
## 2 B 1000
## 3 C 1200
## 4 D 900
## 5 E 1500
小结:mutate()保留所有列,transmute()只保留指定列。
根据条件创建新变量。
# 使用ifelse()创建条件列
transform_data$price_level <- ifelse(
transform_data$price > 200, # 条件
"高价", # 条件为TRUE时的值
"低价" # 条件为FALSE时的值
)
# 使用case_when()处理多条件
transform_data <- transform_data %>%
mutate(
category = case_when(
price >= 250 ~ "高端", # 条件1
price >= 150 ~ "中端", # 条件2
TRUE ~ "低端" # 默认值
)
)
## mutate: new variable 'category' (character) with 3 unique values and 0% NA
transform_data
## product price quantity revenue unit_cost profit price_level category
## 1 A 100 10 1000 60 400 低价 低端
## 2 B 200 5 1000 120 400 低价 中端
## 3 C 150 8 1200 90 480 低价 中端
## 4 D 300 3 900 180 360 高价 高端
## 5 E 250 6 1500 150 600 高价 高端
小结:ifelse()适合二分类,case_when()适合多分类。
窗口函数用于计算相对位置或累计值。
# 创建时间序列数据
window_data <- data.frame(
month = 1:12,
sales = c(100, 120, 150, 130, 160, 180,
200, 190, 210, 230, 250, 280)
)
# lag()获取前一个值
window_data$prev_sales <- lag(window_data$sales)
# lead()获取后一个值
window_data$next_sales <- lead(window_data$sales)
# 计算环比增长
window_data$growth <- (window_data$sales - window_data$prev_sales) /
window_data$prev_sales * 100
# cumsum()累计求和
window_data$cum_sales <- cumsum(window_data$sales)
# cummean()累计均值
window_data$cum_avg <- cummean(window_data$sales)
# cummax()累计最大值
window_data$cum_max <- cummax(window_data$sales)
# cummin()累计最小值
window_data$cum_min <- cummin(window_data$sales)
window_data
## month sales prev_sales next_sales growth cum_sales cum_avg cum_max
## 1 1 100 NA 120 NA 100 100.0000 100
## 2 2 120 100 150 20.000000 220 110.0000 120
## 3 3 150 120 130 25.000000 370 123.3333 150
## 4 4 130 150 160 -13.333333 500 125.0000 150
## 5 5 160 130 180 23.076923 660 132.0000 160
## 6 6 180 160 200 12.500000 840 140.0000 180
## 7 7 200 180 190 11.111111 1040 148.5714 200
## 8 8 190 200 210 -5.000000 1230 153.7500 200
## 9 9 210 190 230 10.526316 1440 160.0000 210
## 10 10 230 210 250 9.523810 1670 167.0000 230
## 11 11 250 230 280 8.695652 1920 174.5455 250
## 12 12 280 250 NA 12.000000 2200 183.3333 280
## cum_min
## 1 100
## 2 100
## 3 100
## 4 100
## 5 100
## 6 100
## 7 100
## 8 100
## 9 100
## 10 100
## 11 100
## 12 100
小结:窗口函数在时间序列分析中非常有用。
按组进行汇总统计。
# 创建分组数据
group_data <- data.frame(
department = c("销售", "技术", "销售", "技术", "财务",
"销售", "技术", "财务", "销售", "技术"),
employee = c("张三", "李四", "王五", "赵六", "钱七",
"孙八", "周九", "吴十", "郑一", "陈二"),
salary = c(5000, 8000, 5500, 9000, 7000,
4500, 10000, 7500, 5200, 8500)
)
# 按部门分组计算平均薪资
group_data %>%
group_by(department) %>%
summarise(
avg_salary = mean(salary),
total_salary = sum(salary),
count = n(),
max_salary = max(salary),
min_salary = min(salary)
)
## group_by: one grouping variable (department)
## summarise: now 3 rows and 6 columns, ungrouped
## # A tibble: 3 × 6
## department avg_salary total_salary count max_salary min_salary
## <chr> <dbl> <dbl> <int> <dbl> <dbl>
## 1 技术 8875 35500 4 10000 8000
## 2 财务 7250 14500 2 7500 7000
## 3 销售 5050 20200 4 5500 4500
小结:group_by()配合summarise()是分组汇总的标准模式。
常用的汇总统计函数。
# 常用汇总函数
group_data %>%
group_by(department) %>%
summarise(
n = n(), # 计数
n_distinct_emp = n_distinct(employee), # 唯一值计数
sum_sal = sum(salary), # 求和
mean_sal = mean(salary), # 均值
median_sal = median(salary), # 中位数
sd_sal = sd(salary), # 标准差
var_sal = var(salary), # 方差
min_sal = min(salary), # 最小值
max_sal = max(salary), # 最大值
range_sal = max(salary) - min(salary), # 极差
first_emp = first(employee), # 第一个值
last_emp = last(employee) # 最后一个值
)
## group_by: one grouping variable (department)
## summarise: now 3 rows and 13 columns, ungrouped
## # A tibble: 3 × 13
## department n n_distinct_emp sum_sal mean_sal median_sal sd_sal var_sal
## <chr> <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 技术 4 4 35500 8875 8750 854. 729167.
## 2 财务 2 2 14500 7250 7250 354. 125000
## 3 销售 4 4 20200 5050 5100 420. 176667.
## # ℹ 5 more variables: min_sal <dbl>, max_sal <dbl>, range_sal <dbl>,
## # first_emp <chr>, last_emp <chr>
小结:n()统计行数,n_distinct()统计唯一值数量。
使用across()对多列应用相同函数。
# 创建多列数据
multi_data <- data.frame(
group = c("A", "A", "B", "B", "C", "C"),
x = c(1, 2, 3, 4, 5, 6),
y = c(10, 20, 30, 40, 50, 60),
z = c(100, 200, 300, 400, 500, 600)
)
# 对所有数值列计算均值
multi_data %>%
group_by(group) %>%
summarise(
across(where(is.numeric), mean, .names = "mean_{.col}")
)
## group_by: one grouping variable (group)
## summarise: now 3 rows and 4 columns, ungrouped
## # A tibble: 3 × 4
## group mean_x mean_y mean_z
## <chr> <dbl> <dbl> <dbl>
## 1 A 1.5 15 150
## 2 B 3.5 35 350
## 3 C 5.5 55 550
# 对指定列计算多个统计量
multi_data %>%
group_by(group) %>%
summarise(
across(c(x, y),
list(mean = mean, sd = sd),
.names = "{.col}_{.fn}")
)
## group_by: one grouping variable (group)
## summarise: now 3 rows and 5 columns, ungrouped
## # A tibble: 3 × 5
## group x_mean x_sd y_mean y_sd
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 A 1.5 0.707 15 7.07
## 2 B 3.5 0.707 35 7.07
## 3 C 5.5 0.707 55 7.07
小结:across()可以批量处理多列,非常高效。
将汇总结果合并回原数据框。
# 使用add_count()添加计数列
group_data %>%
add_count(department, name = "dept_count")
## add_count: new variable 'dept_count' (integer) with 2 unique values and 0% NA
## department employee salary dept_count
## 1 销售 张三 5000 4
## 2 技术 李四 8000 4
## 3 销售 王五 5500 4
## 4 技术 赵六 9000 4
## 5 财务 钱七 7000 2
## 6 销售 孙八 4500 4
## 7 技术 周九 10000 4
## 8 财务 吴十 7500 2
## 9 销售 郑一 5200 4
## 10 技术 陈二 8500 4
# 使用add_tally()添加计数列(需要先分组)
group_data %>%
group_by(department) %>%
add_tally(name = "dept_count")
## group_by: one grouping variable (department)
## add_tally (grouped): new variable 'dept_count' (integer) with 2 unique values and 0% NA
## # A tibble: 10 × 4
## # Groups: department [3]
## department employee salary dept_count
## <chr> <chr> <dbl> <int>
## 1 销售 张三 5000 4
## 2 技术 李四 8000 4
## 3 销售 王五 5500 4
## 4 技术 赵六 9000 4
## 5 财务 钱七 7000 2
## 6 销售 孙八 4500 4
## 7 技术 周九 10000 4
## 8 财务 吴十 7500 2
## 9 销售 郑一 5200 4
## 10 技术 陈二 8500 4
# 计算组内占比
group_data %>%
group_by(department) %>%
mutate(
dept_total = sum(salary),
dept_avg = mean(salary),
salary_ratio = salary / dept_total
)
## group_by: one grouping variable (department)
## mutate (grouped): new variable 'dept_total' (double) with 3 unique values and 0% NA
## new variable 'dept_avg' (double) with 3 unique values and 0% NA
## new variable 'salary_ratio' (double) with 10 unique values and 0% NA
## # A tibble: 10 × 6
## # Groups: department [3]
## department employee salary dept_total dept_avg salary_ratio
## <chr> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 销售 张三 5000 20200 5050 0.248
## 2 技术 李四 8000 35500 8875 0.225
## 3 销售 王五 5500 20200 5050 0.272
## 4 技术 赵六 9000 35500 8875 0.254
## 5 财务 钱七 7000 14500 7250 0.483
## 6 销售 孙八 4500 20200 5050 0.223
## 7 技术 周九 10000 35500 8875 0.282
## 8 财务 吴十 7500 14500 7250 0.517
## 9 销售 郑一 5200 20200 5050 0.257
## 10 技术 陈二 8500 35500 8875 0.239
小结:add_count()和add_tally()可以快速添加计数列。
在滑动窗口内进行计算。
# 使用zoo包进行滚动计算
library(zoo)
# 创建时间序列数据
ts_data <- data.frame(
day = 1:10,
value = c(10, 12, 15, 11, 18, 20, 16, 22, 19, 25)
)
# 滚动平均(3天窗口)
ts_data$rolling_mean <- rollmean(ts_data$value, k = 3, fill = NA, align = "right")
# 滚动求和
ts_data$rolling_sum <- rollsum(ts_data$value, k = 3, fill = NA, align = "right")
# 滚动标准差
ts_data$rolling_sd <- rollapply(ts_data$value, width = 3, FUN = sd, fill = NA, align = "right")
ts_data
小结:zoo包提供了丰富的滚动计算函数,适合时间序列分析。
对每行的多列进行计算。
# 创建多列数据
row_data <- data.frame(
id = 1:5,
score1 = c(80, 85, 90, 75, 88),
score2 = c(75, 82, 88, 80, 85),
score3 = c(85, 88, 92, 78, 90)
)
# 使用rowwise()进行跨行计算
row_data %>%
rowwise() %>%
mutate(
total = sum(c_across(starts_with("score"))),
average = mean(c_across(starts_with("score"))),
max_score = max(c_across(starts_with("score")))
)
## mutate: new variable 'total' (double) with 5 unique values and 0% NA
## new variable 'average' (double) with 5 unique values and 0% NA
## new variable 'max_score' (double) with 5 unique values and 0% NA
## # A tibble: 5 × 7
## # Rowwise:
## id score1 score2 score3 total average max_score
## <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 80 75 85 240 80 85
## 2 2 85 82 88 255 85 88
## 3 3 90 88 92 270 90 92
## 4 4 75 80 78 233 77.7 80
## 5 5 88 85 90 263 87.7 90
# 使用rowSums()和rowMeans()(更高效)
row_data$total <- rowSums(row_data[, c("score1", "score2", "score3")])
row_data$average <- rowMeans(row_data[, c("score1", "score2", "score3")])
row_data
## id score1 score2 score3 total average
## 1 1 80 75 85 240 80.00000
## 2 2 85 82 88 255 85.00000
## 3 3 90 88 92 270 90.00000
## 4 4 75 80 78 233 77.66667
## 5 5 88 85 90 263 87.66667
小结:rowwise()灵活但较慢,rowSums()等函数效率更高。
累计计算。
# 创建数据
cum_data <- data.frame(
month = 1:6,
revenue = c(100, 150, 120, 180, 200, 160)
)
# 累计求和
cum_data$cum_revenue <- cumsum(cum_data$revenue)
# 累计乘积
cum_data$cum_product <- cumprod(cum_data$revenue / 100 + 1)
# 累计最大值
cum_data$cum_max <- cummax(cum_data$revenue)
# 累计最小值
cum_data$cum_min <- cummin(cum_data$revenue)
cum_data
## month revenue cum_revenue cum_product cum_max cum_min
## 1 1 100 100 2.00 100 100
## 2 2 150 250 5.00 150 100
## 3 3 120 370 11.00 150 100
## 4 4 180 550 30.80 180 100
## 5 5 200 750 92.40 200 100
## 6 6 160 910 240.24 200 100
小结:累计函数在财务分析和时间序列中常用。
| 操作 | 函数 | 说明 |
|---|---|---|
| 添加列 | mutate() |
创建新变量 |
| 条件列 | ifelse()、case_when() |
条件创建 |
| 窗口函数 | lag()、lead() |
前后值 |
| 累计函数 | cumsum()、cumprod() |
累计计算 |
| 分组汇总 | group_by() + summarise() |
分组统计 |
| 批量处理 | across() |
多列操作 |
实际分析中,数据往往分散在多个表格中。本章介绍如何合并和连接数据。
将多个数据框纵向合并(增加行)。
# 创建两个数据框
df1 <- data.frame(
id = 1:3,
name = c("张三", "李四", "王五")
)
df2 <- data.frame(
id = 4:6,
name = c("赵六", "钱七", "孙八")
)
# 使用rbind()合并
rbind(df1, df2)
## id name
## 1 1 张三
## 2 2 李四
## 3 3 王五
## 4 4 赵六
## 5 5 钱七
## 6 6 孙八
# 使用dplyr的bind_rows()
library(dplyr)
bind_rows(df1, df2)
## id name
## 1 1 张三
## 2 2 李四
## 3 3 王五
## 4 4 赵六
## 5 5 钱七
## 6 6 孙八
# bind_rows()可以合并列名不完全相同的数据框
df3 <- data.frame(
id = 7:8,
name = c("周九", "吴十"),
age = c(25, 30) # 新增列
)
bind_rows(df1, df3) # 自动填充NA
## id name age
## 1 1 张三 NA
## 2 2 李四 NA
## 3 3 王五 NA
## 4 7 周九 25
## 5 8 吴十 30
小结:bind_rows()比rbind()更灵活,可以处理列名不完全相同的情况。
将多个数据框横向合并(增加列)。
# 创建两个数据框
df_a <- data.frame(
id = 1:3,
name = c("张三", "李四", "王五")
)
df_b <- data.frame(
score = c(85, 90, 78),
grade = c("B", "A", "C")
)
# 使用cbind()合并
cbind(df_a, df_b)
## id name score grade
## 1 1 张三 85 B
## 2 2 李四 90 A
## 3 3 王五 78 C
# 使用dplyr的bind_cols()
bind_cols(df_a, df_b)
## id name score grade
## 1 1 张三 85 B
## 2 2 李四 90 A
## 3 3 王五 78 C
小结:按列合并时,两个数据框的行数必须相同,且行顺序要对应。
只保留两个表中都有匹配的行。
# 创建两个数据框
employees <- data.frame(
emp_id = c(1, 2, 3, 4, 5),
name = c("张三", "李四", "王五", "赵六", "钱七"),
dept_id = c(101, 102, 101, 103, 102)
)
departments <- data.frame(
dept_id = c(101, 102, 104),
dept_name = c("销售部", "技术部", "财务部")
)
# 内连接:只保留两个表都有匹配的记录
inner_join(employees, departments, by = "dept_id")
## inner_join: added one column (dept_name)
## > rows only in employees (1)
## > rows only in departments (1)
## > matched rows 4
## > ===
## > rows total 4
## emp_id name dept_id dept_name
## 1 1 张三 101 销售部
## 2 2 李四 102 技术部
## 3 3 王五 101 销售部
## 4 5 钱七 102 技术部
小结:内连接会丢失没有匹配的记录。
保留左表所有行,右表无匹配则为NA。
# 左连接:保留左表(employees)所有行
left_join(employees, departments, by = "dept_id")
## left_join: added one column (dept_name)
## > rows only in employees 1
## > rows only in departments (1)
## > matched rows 4
## > ===
## > rows total 5
## emp_id name dept_id dept_name
## 1 1 张三 101 销售部
## 2 2 李四 102 技术部
## 3 3 王五 101 销售部
## 4 4 赵六 103 <NA>
## 5 5 钱七 102 技术部
小结:左连接是最常用的连接方式,保留主表所有记录。
保留右表所有行,左表无匹配则为NA。
# 右连接:保留右表(departments)所有行
right_join(employees, departments, by = "dept_id")
## right_join: added one column (dept_name)
## > rows only in employees (1)
## > rows only in departments 1
## > matched rows 4 (includes duplicates)
## > ===
## > rows total 5
## emp_id name dept_id dept_name
## 1 1 张三 101 销售部
## 2 2 李四 102 技术部
## 3 3 王五 101 销售部
## 4 5 钱七 102 技术部
## 5 NA <NA> 104 财务部
小结:右连接等价于调换表顺序的左连接。
保留两个表所有行。
# 全连接:保留两个表所有行
full_join(employees, departments, by = "dept_id")
## full_join: added one column (dept_name)
## > rows only in employees 1
## > rows only in departments 1
## > matched rows 4
## > ===
## > rows total 6
## emp_id name dept_id dept_name
## 1 1 张三 101 销售部
## 2 2 李四 102 技术部
## 3 3 王五 101 销售部
## 4 4 赵六 103 <NA>
## 5 5 钱七 102 技术部
## 6 NA <NA> 104 财务部
小结:全连接保留所有记录,无匹配处填充NA。
只保留左表中在右表有匹配的行,不添加右表列。
# 半连接:只返回employees中在departments有匹配的行
semi_join(employees, departments, by = "dept_id")
## semi_join: added no columns
## > rows only in employees (1)
## > rows only in departments (1)
## > matched rows 4
## > ===
## > rows total 4
## emp_id name dept_id
## 1 1 张三 101
## 2 2 李四 102
## 3 3 王五 101
## 4 5 钱七 102
小结:半连接用于筛选,不添加新列。
只保留左表中在右表没有匹配的行。
# 反连接:返回employees中在departments没有匹配的行
anti_join(employees, departments, by = "dept_id")
## anti_join: added no columns
## > rows only in employees 1
## > rows only in departments (1)
## > matched rows (4)
## > ===
## > rows total 1
## emp_id name dept_id
## 1 4 赵六 103
小结:反连接常用于找出缺失的记录。
使用多个列作为连接键。
# 创建示例数据
sales1 <- data.frame(
year = c(2023, 2023, 2024, 2024),
quarter = c(1, 2, 1, 2),
sales = c(100, 150, 120, 180)
)
sales2 <- data.frame(
year = c(2023, 2023, 2024, 2024),
quarter = c(1, 2, 1, 2),
profit = c(20, 30, 25, 40)
)
# 基于year和quarter两个键连接
left_join(sales1, sales2, by = c("year", "quarter"))
## left_join: added one column (profit)
## > rows only in sales1 0
## > rows only in sales2 (0)
## > matched rows 4
## > ===
## > rows total 4
## year quarter sales profit
## 1 2023 1 100 20
## 2 2023 2 150 30
## 3 2024 1 120 25
## 4 2024 2 180 40
小结:多键连接可以更精确地匹配记录。
当键值不完全匹配时,可以使用模糊匹配。
# 使用fuzzyjoin包进行模糊匹配
# 如果没有安装,先运行:install.packages("fuzzyjoin")
library(fuzzyjoin)
# 创建示例数据
company1 <- data.frame(
name = c("阿里巴巴", "腾讯科技", "百度公司")
)
company2 <- data.frame(
name = c("阿里巴巴集团", "腾讯", "百度"),
stock_code = c("BABA", "0700", "BIDU")
)
# 字符串差异连接
stringdist_left_join(company1, company2,
by = "name",
max_dist = 3, # 最大编辑距离
distance_col = "dist")
小结:fuzzyjoin包提供了多种模糊匹配方法,适合名称不完全一致的数据。
| 连接类型 | 函数 | 保留记录 |
|---|---|---|
| 内连接 | inner_join() |
两表都有匹配的 |
| 左连接 | left_join() |
左表全部 |
| 右连接 | right_join() |
右表全部 |
| 全连接 | full_join() |
两表全部 |
| 半连接 | semi_join() |
左表有匹配的 |
| 反连接 | anti_join() |
左表无匹配的 |
数据重塑是数据整理的重要技能。本章介绍长宽格式转换和数据处理技巧。
将宽格式数据转换为长格式。
# 创建宽格式数据
wide_data <- data.frame(
name = c("张三", "李四", "王五"),
math = c(85, 90, 78),
english = c(88, 85, 92),
physics = c(90, 88, 85)
)
# 使用pivot_longer()转换为长格式
library(tidyr)
long_data <- pivot_longer(
wide_data,
cols = c(math, english, physics), # 需要转换的列
names_to = "subject", # 新列名(存储原列名)
values_to = "score" # 新列名(存储原值)
)
## pivot_longer: reorganized (math, english, physics) into (subject, score) [was
## 3x4, now 9x3]
long_data
## # A tibble: 9 × 3
## name subject score
## <chr> <chr> <dbl>
## 1 张三 math 85
## 2 张三 english 88
## 3 张三 physics 90
## 4 李四 math 90
## 5 李四 english 85
## 6 李四 physics 88
## 7 王五 math 78
## 8 王五 english 92
## 9 王五 physics 85
小结:pivot_longer()是gather()的现代替代,语法更清晰。
将长格式数据转换为宽格式。
# 使用pivot_wider()转换为宽格式
pivot_wider(
long_data,
names_from = subject, # 列名来源
values_from = score # 值来源
)
## pivot_wider: reorganized (subject, score) into (math, english, physics) [was
## 9x3, now 3x4]
## # A tibble: 3 × 4
## name math english physics
## <chr> <dbl> <dbl> <dbl>
## 1 张三 85 88 90
## 2 李四 90 85 88
## 3 王五 78 92 85
# 旧版gather()和spread()函数
# gather():宽转长
gather(wide_data, key = "subject", value = "score", math:physics)
## gather: reorganized (math, english, physics) into (subject, score) [was 3x4,
## now 9x3]
## name subject score
## 1 张三 math 85
## 2 李四 math 90
## 3 王五 math 78
## 4 张三 english 88
## 5 李四 english 85
## 6 王五 english 92
## 7 张三 physics 90
## 8 李四 physics 88
## 9 王五 physics 85
# spread():长转宽
spread(long_data, key = subject, value = score)
## spread: reorganized (subject, score) into (english, math, physics) [was 9x3,
## now 3x4]
## # A tibble: 3 × 4
## name english math physics
## <chr> <dbl> <dbl> <dbl>
## 1 李四 85 90 88
## 2 王五 92 78 85
## 3 张三 88 85 90
小结:pivot_wider()是spread()的现代替代,功能更强大。
将一列拆分为多列。
# 创建需要分隔的数据
split_data <- data.frame(
id = 1:3,
info = c("张三_25_北京", "李四_30_上海", "王五_28_广州")
)
# 使用separate()分隔列
separate(
split_data,
col = info, # 要分隔的列
into = c("name", "age", "city"), # 新列名
sep = "_" # 分隔符
)
## id name age city
## 1 1 张三 25 北京
## 2 2 李四 30 上海
## 3 3 王五 28 广州
# 使用extract()通过正则表达式提取
extract(
split_data,
col = info,
into = c("name", "age", "city"),
regex = "(.+)_(.+)_(.+)"
)
## id name age city
## 1 1 张三 25 北京
## 2 2 李四 30 上海
## 3 3 王五 28 广州
小结:separate()按分隔符拆分,extract()用正则表达式提取。
将多列合并为一列。
# 创建需要合并的数据
merge_data <- data.frame(
name = c("张三", "李四", "王五"),
age = c(25, 30, 28),
city = c("北京", "上海", "广州")
)
# 使用unite()合并列
unite(
merge_data,
col = "info", # 新列名
name, age, city, # 要合并的列
sep = "_" # 分隔符
)
## info
## 1 张三_25_北京
## 2 李四_30_上海
## 3 王五_28_广州
# 删除原列(默认行为)
unite(merge_data, "info", name:city, sep = "-", remove = TRUE)
## info
## 1 张三-25-北京
## 2 李四-30-上海
## 3 王五-28-广州
小结:unite()是separate()的逆操作,用于合并多列。
处理嵌套数据结构。
# 创建嵌套数据
nested_data <- data.frame(
group = c("A", "A", "B", "B"),
value = c(1, 2, 3, 4)
)
# nest()将数据嵌套为列表列
nested <- nested_data %>%
group_by(group) %>%
nest()
# 查看嵌套结构
nested
# unnest()取消嵌套
unnest(nested, cols = c(data))
小结:嵌套数据适合分组处理和函数式编程。
| 操作 | 函数 | 说明 |
|---|---|---|
| 宽转长 | pivot_longer() |
宽格式转长格式 |
| 长转宽 | pivot_wider() |
长格式转宽格式 |
| 分隔列 | separate() |
一列拆多列 |
| 合并列 | unite() |
多列合一列 |
| 嵌套 | nest() |
创建嵌套数据 |
| 取消嵌套 | unnest() |
展开嵌套数据 |
字符和日期是数据分析中常见的数据类型。本章介绍字符和日期的处理方法。
处理字符串中的多余空格。
# 创建含有空格的字符串
text_data <- c(" hello ", "world ", " R语言")
# trimws()去除首尾空格
trimws(text_data)
## [1] "hello" "world" "R语言"
# stringr包提供更多字符串处理函数
library(stringr)
# str_trim()去除首尾空格
str_trim(text_data)
## [1] "hello" "world" "R语言"
# str_squish()去除首尾空格并压缩内部多余空格
text_with_space <- c("hello world", "R 语言")
str_squish(text_with_space)
## [1] "hello world" "R 语言"
小结:trimws()是R内置函数,stringr包功能更丰富。
替换字符串中的内容。
# 创建示例字符串
text <- "Hello World, Hello R"
# gsub()全局替换
gsub("Hello", "Hi", text)
## [1] "Hi World, Hi R"
# sub()只替换第一个匹配
sub("Hello", "Hi", text)
## [1] "Hi World, Hello R"
# stringr包的str_replace()
str_replace(text, "Hello", "Hi") # 替换第一个
## [1] "Hi World, Hello R"
# str_replace_all()替换所有
str_replace_all(text, "Hello", "Hi")
## [1] "Hi World, Hi R"
小结:gsub()替换所有匹配,sub()只替换第一个。
将字符串分割成多个部分。
# 创建示例字符串
text <- "apple,banana,orange"
# str_split()分割字符串
str_split(text, ",")
## [[1]]
## [1] "apple" "banana" "orange"
# 限制分割次数
str_split(text, ",", n = 2)
## [[1]]
## [1] "apple" "banana,orange"
# str_split_fixed()返回矩阵
str_split_fixed(text, ",", n = 3)
## [,1] [,2] [,3]
## [1,] "apple" "banana" "orange"
# separate_rows()将分割结果展开为多行
library(tidyr)
df <- data.frame(id = 1, fruits = "apple,banana,orange")
separate_rows(df, fruits, sep = ",")
## # A tibble: 3 × 2
## id fruits
## <dbl> <chr>
## 1 1 apple
## 2 1 banana
## 3 1 orange
小结:str_split()返回列表,str_split_fixed()返回矩阵。
从字符串中提取内容。
# 创建示例字符串
text <- c("apple123", "banana456", "orange789")
# str_extract()提取第一个匹配
str_extract(text, "[0-9]+") # 提取数字
## [1] "123" "456" "789"
# str_extract_all()提取所有匹配
str_extract_all(text, "[a-z]+") # 提取字母
## [[1]]
## [1] "apple"
##
## [[2]]
## [1] "banana"
##
## [[3]]
## [1] "orange"
# str_sub()按位置提取
str_sub(text, start = 1, end = 5)
## [1] "apple" "banan" "orang"
# 负数表示从末尾开始
str_sub(text, start = -3) # 提取最后3个字符
## [1] "123" "456" "789"
小结:str_extract()使用正则表达式,str_sub()按位置提取。
转换字符串的大小写。
# 创建示例字符串
text <- c("Hello World", "R LANGUAGE")
# toupper()转大写
toupper(text)
## [1] "HELLO WORLD" "R LANGUAGE"
# tolower()转小写
tolower(text)
## [1] "hello world" "r language"
# str_to_title()首字母大写
str_to_title("hello world")
## [1] "Hello World"
# str_to_upper()和str_to_lower()
str_to_upper(text)
## [1] "HELLO WORLD" "R LANGUAGE"
str_to_lower(text)
## [1] "hello world" "r language"
小结:R内置函数简单直接,stringr函数支持本地化设置。
将多个字符串连接在一起。
# paste()和paste0()拼接字符串
paste("Hello", "World") # 默认用空格分隔
## [1] "Hello World"
paste("Hello", "World", sep = "_") # 自定义分隔符
## [1] "Hello_World"
paste0("Hello", "World") # 无分隔符
## [1] "HelloWorld"
# str_c()拼接字符串
str_c("Hello", "World", sep = " ")
## [1] "Hello World"
# 拼接向量
fruits <- c("apple", "banana", "orange")
paste(fruits, collapse = ", ") # 将向量元素合并
## [1] "apple, banana, orange"
# str_flatten()展平字符串向量
str_flatten(fruits, collapse = ", ")
## [1] "apple, banana, orange"
小结:paste()是R内置函数,str_c()来自stringr包。
检测字符串是否匹配模式。
# 创建示例字符串
text <- c("apple", "banana", "orange", "grape")
# str_detect()检测是否匹配
str_detect(text, "a") # 是否包含"a"
## [1] TRUE TRUE TRUE TRUE
# grepl()检测是否匹配(R内置)
grepl("a", text)
## [1] TRUE TRUE TRUE TRUE
# 返回匹配的位置
str_which(text, "a")
## [1] 1 2 3 4
# 返回匹配的数量
str_count(text, "a")
## [1] 1 3 1 1
小结:str_detect()返回逻辑向量,str_which()返回位置。
获取字符串的长度。
# 创建示例字符串
text <- c("Hello", "世界", "R语言")
# nchar()获取字符数
nchar(text)
## [1] 5 2 3
# str_length()获取字符数
str_length(text)
## [1] 5 2 3
# str_width()获取显示宽度
str_width(text)
## [1] 5 4 5
小结:nchar()是R内置函数,注意中文字符的长度计算。
将字符串转换为日期。
# 创建日期字符串
date_strings <- c("2024-01-15", "2024-06-20", "2024-12-31")
# as.Date()转换为日期
as.Date(date_strings)
## [1] "2024-01-15" "2024-06-20" "2024-12-31"
# 指定格式
as.Date("15/01/2024", format = "%d/%m/%Y")
## [1] "2024-01-15"
# lubridate包提供更便捷的函数
library(lubridate)
# ymd()解析年月日格式
ymd("2024-01-15")
## [1] "2024-01-15"
ymd("20240115")
## [1] "2024-01-15"
ymd("2024/01/15")
## [1] "2024-01-15"
# mdy()解析月日年格式
mdy("01-15-2024")
## [1] "2024-01-15"
# dmy()解析日月年格式
dmy("15-01-2024")
## [1] "2024-01-15"
小结:lubridate包的函数更直观,能自动识别多种格式。
从日期中提取年、月、日等成分。
# 创建日期
date <- ymd("2024-06-15")
# 提取年份
year(date)
## [1] 2024
# 提取月份
month(date)
## [1] 6
month(date, label = TRUE) # 返回月份名称
## [1] 6月
## 12 Levels: 1月 < 2月 < 3月 < 4月 < 5月 < 6月 < 7月 < 8月 < 9月 < ... < 12月
# 提取日
day(date)
## [1] 15
# 提取星期
wday(date)
## [1] 7
wday(date, label = TRUE) # 返回星期名称
## [1] 周六
## Levels: 周日 < 周一 < 周二 < 周三 < 周四 < 周五 < 周六
# 提取季度
quarter(date)
## [1] 2
# 提取一年中的第几天
yday(date)
## [1] 167
# 提取一周中的第几天
wday(date)
## [1] 7
小结:lubridate包提供了丰富的日期成分提取函数。
进行日期的加减运算。
# 创建日期
date <- ymd("2024-06-15")
# 加减天数
date + 1
## [1] "2024-06-16"
date - 7
## [1] "2024-06-08"
# 加减月数
date %m+% months(1)
## [1] "2024-07-15"
date %m-% months(2)
## [1] "2024-04-15"
# 加减年数
date + years(1)
## [1] "2025-06-15"
# 计算日期差
date1 <- ymd("2024-01-01")
date2 <- ymd("2024-06-15")
# difftime()计算时间差
difftime(date2, date1, units = "days")
## Time difference of 166 days
# 使用减号计算
date2 - date1
## Time difference of 166 days
# 计算间隔
interval(date1, date2)
## [1] 2024-01-01 UTC--2024-06-15 UTC
# 计算间隔的时长
interval(date1, date2) / days(1)
## [1] 166
小结:difftime()计算时间差,lubridate提供更丰富的日期运算。
处理时间序列数据。
# 创建时间序列数据
ts_data <- c(10, 20, 30, 40, 50, 60)
# ts()创建时间序列对象
ts_obj <- ts(ts_data, start = c(2024, 1), frequency = 12) # 月度数据
ts_obj
# xts包提供更强大的时间序列功能
# 如果没有安装,先运行:install.packages("xts")
library(xts)
# 创建xts对象
dates <- ymd(c("2024-01-01", "2024-02-01", "2024-03-01"))
xts_obj <- xts(ts_data[1:3], order.by = dates)
xts_obj
# zoo包的基础转换
library(zoo)
zoo_obj <- zoo(ts_data[1:3], order.by = dates)
zoo_obj
小结:ts是R内置时间序列类,xts和zoo提供更强大的功能。
| 操作 | 函数 | 说明 |
|---|---|---|
| 去空格 | trimws()、str_trim() |
去除首尾空格 |
| 替换 | gsub()、str_replace() |
替换字符串 |
| 分割 | str_split() |
分割字符串 |
| 提取 | str_extract()、str_sub() |
提取子串 |
| 拼接 | paste()、str_c() |
连接字符串 |
| 日期解析 | ymd()、as.Date() |
字符串转日期 |
| 日期提取 | year()、month() |
提取日期成分 |
| 日期计算 | difftime()、interval() |
日期运算 |
分组操作是数据分析的核心技能。本章介绍分组后的各种操作。
使用group_by()创建分组数据。
# 创建示例数据
group_data <- data.frame(
department = c("销售", "技术", "销售", "技术", "财务",
"销售", "技术", "财务", "销售", "技术"),
employee = c("张三", "李四", "王五", "赵六", "钱七",
"孙八", "周九", "吴十", "郑一", "陈二"),
salary = c(5000, 8000, 5500, 9000, 7000,
4500, 10000, 7500, 5200, 8500)
)
# 创建分组数据框
grouped <- group_by(group_data, department)
## group_by: one grouping variable (department)
# 查看分组信息
group_vars(grouped)
## [1] "department"
n_groups(grouped)
## [1] 3
# 使用ungroup()取消分组
ungroup(grouped)
## ungroup: no grouping variables remain
## # A tibble: 10 × 3
## department employee salary
## <chr> <chr> <dbl>
## 1 销售 张三 5000
## 2 技术 李四 8000
## 3 销售 王五 5500
## 4 技术 赵六 9000
## 5 财务 钱七 7000
## 6 销售 孙八 4500
## 7 技术 周九 10000
## 8 财务 吴十 7500
## 9 销售 郑一 5200
## 10 技术 陈二 8500
小结:group_by()创建分组,ungroup()取消分组。
在组内进行筛选。
# 筛选每个部门薪资最高的员工
group_data %>%
group_by(department) %>%
filter(salary == max(salary))
## group_by: one grouping variable (department)
## filter (grouped): removed 7 rows (70%), 3 rows remaining (removed 0 groups, 3 groups remaining)
## # A tibble: 3 × 3
## # Groups: department [3]
## department employee salary
## <chr> <chr> <dbl>
## 1 销售 王五 5500
## 2 技术 周九 10000
## 3 财务 吴十 7500
# 筛选薪资高于部门平均值的员工
group_data %>%
group_by(department) %>%
filter(salary > mean(salary))
## group_by: one grouping variable (department)
## filter (grouped): removed 5 rows (50%), 5 rows remaining (removed 0 groups, 3 groups remaining)
## # A tibble: 5 × 3
## # Groups: department [3]
## department employee salary
## <chr> <chr> <dbl>
## 1 销售 王五 5500
## 2 技术 赵六 9000
## 3 技术 周九 10000
## 4 财务 吴十 7500
## 5 销售 郑一 5200
# 筛选每个部门的前2名
group_data %>%
group_by(department) %>%
slice_max(salary, n = 2)
## group_by: one grouping variable (department)
## slice_max (grouped): removed 4 rows (40%), 6 rows remaining (removed 0 groups, 3 groups remaining)
## # A tibble: 6 × 3
## # Groups: department [3]
## department employee salary
## <chr> <chr> <dbl>
## 1 技术 周九 10000
## 2 技术 赵六 9000
## 3 财务 吴十 7500
## 4 财务 钱七 7000
## 5 销售 王五 5500
## 6 销售 郑一 5200
小结:分组后的筛选操作会在每个组内独立进行。
在组内进行排序。
# 每个部门内按薪资降序排列
group_data %>%
group_by(department) %>%
arrange(desc(salary), .by_group = TRUE)
## group_by: one grouping variable (department)
## # A tibble: 10 × 3
## # Groups: department [3]
## department employee salary
## <chr> <chr> <dbl>
## 1 技术 周九 10000
## 2 技术 赵六 9000
## 3 技术 陈二 8500
## 4 技术 李四 8000
## 5 财务 吴十 7500
## 6 财务 钱七 7000
## 7 销售 王五 5500
## 8 销售 郑一 5200
## 9 销售 张三 5000
## 10 销售 孙八 4500
小结:.by_group = TRUE保证结果按组排列。
计算组内排名。
# 计算组内薪资排名
group_data %>%
group_by(department) %>%
mutate(
rank = rank(desc(salary)), # 平均排名
min_rank = min_rank(desc(salary)), # 最小排名
dense_rank = dense_rank(desc(salary)), # 紧凑排名
row_num = row_number(desc(salary)) # 行号排名
)
## group_by: one grouping variable (department)
## mutate (grouped): new variable 'rank' (double) with 4 unique values and 0% NA
## new variable 'min_rank' (integer) with 4 unique values and 0% NA
## new variable 'dense_rank' (integer) with 4 unique values and 0% NA
## new variable 'row_num' (integer) with 4 unique values and 0% NA
## # A tibble: 10 × 7
## # Groups: department [3]
## department employee salary rank min_rank dense_rank row_num
## <chr> <chr> <dbl> <dbl> <int> <int> <int>
## 1 销售 张三 5000 3 3 3 3
## 2 技术 李四 8000 4 4 4 4
## 3 销售 王五 5500 1 1 1 1
## 4 技术 赵六 9000 2 2 2 2
## 5 财务 钱七 7000 2 2 2 2
## 6 销售 孙八 4500 4 4 4 4
## 7 技术 周九 10000 1 1 1 1
## 8 财务 吴十 7500 1 1 1 1
## 9 销售 郑一 5200 2 2 2 2
## 10 技术 陈二 8500 3 3 3 3
小结:不同排名方法处理并列的方式不同。
计算组内比例。
# 计算薪资占部门总薪资的比例
group_data %>%
group_by(department) %>%
mutate(
dept_total = sum(salary),
salary_prop = salary / dept_total * 100
)
## group_by: one grouping variable (department)
## mutate (grouped): new variable 'dept_total' (double) with 3 unique values and 0% NA
## new variable 'salary_prop' (double) with 10 unique values and 0% NA
## # A tibble: 10 × 5
## # Groups: department [3]
## department employee salary dept_total salary_prop
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 销售 张三 5000 20200 24.8
## 2 技术 李四 8000 35500 22.5
## 3 销售 王五 5500 20200 27.2
## 4 技术 赵六 9000 35500 25.4
## 5 财务 钱七 7000 14500 48.3
## 6 销售 孙八 4500 20200 22.3
## 7 技术 周九 10000 35500 28.2
## 8 财务 吴十 7500 14500 51.7
## 9 销售 郑一 5200 20200 25.7
## 10 技术 陈二 8500 35500 23.9
# 使用prop.table()计算比例
group_data %>%
group_by(department) %>%
mutate(prop = prop.table(salary))
## group_by: one grouping variable (department)
## mutate (grouped): new variable 'prop' (double) with 10 unique values and 0% NA
## # A tibble: 10 × 4
## # Groups: department [3]
## department employee salary prop
## <chr> <chr> <dbl> <dbl>
## 1 销售 张三 5000 0.248
## 2 技术 李四 8000 0.225
## 3 销售 王五 5500 0.272
## 4 技术 赵六 9000 0.254
## 5 财务 钱七 7000 0.483
## 6 销售 孙八 4500 0.223
## 7 技术 周九 10000 0.282
## 8 财务 吴十 7500 0.517
## 9 销售 郑一 5200 0.257
## 10 技术 陈二 8500 0.239
小结:组内比例常用于分析贡献度。
计算组内累计值。
# 创建时间序列数据
time_data <- data.frame(
department = c("A", "A", "A", "B", "B", "B"),
month = c(1, 2, 3, 1, 2, 3),
sales = c(100, 150, 120, 80, 90, 110)
)
# 计算组内累计销售额
time_data %>%
group_by(department) %>%
arrange(month) %>%
mutate(
cum_sales = cumsum(sales),
cum_avg = cummean(sales)
)
## group_by: one grouping variable (department)
## mutate (grouped): new variable 'cum_sales' (double) with 6 unique values and 0% NA
## new variable 'cum_avg' (double) with 6 unique values and 0% NA
## # A tibble: 6 × 5
## # Groups: department [2]
## department month sales cum_sales cum_avg
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 A 1 100 100 100
## 2 B 1 80 80 80
## 3 A 2 150 250 125
## 4 B 2 90 170 85
## 5 A 3 120 370 123.
## 6 B 3 110 280 93.3
小结:分组累计常用于时间序列分析。
在组内填充缺失值。
# 创建含缺失值的分组数据
na_group_data <- data.frame(
group = c("A", "A", "A", "B", "B", "B"),
value = c(10, NA, 30, NA, 50, NA)
)
# 在组内前向填充
na_group_data %>%
group_by(group) %>%
fill(value, .direction = "down")
## group_by: one grouping variable (group)
## fill (grouped): changed 2 values (33%) of 'value' (2 fewer NAs)
## # A tibble: 6 × 2
## # Groups: group [2]
## group value
## <chr> <dbl>
## 1 A 10
## 2 A 10
## 3 A 30
## 4 B NA
## 5 B 50
## 6 B 50
# 在组内双向填充
na_group_data %>%
group_by(group) %>%
fill(value, .direction = "downup")
## group_by: one grouping variable (group)
## fill (grouped): changed 3 values (50%) of 'value' (3 fewer NAs)
## # A tibble: 6 × 2
## # Groups: group [2]
## group value
## <chr> <dbl>
## 1 A 10
## 2 A 10
## 3 A 30
## 4 B 50
## 5 B 50
## 6 B 50
小结:分组填充确保填充只在同组内进行。
在分组后应用自定义函数。
# 定义自定义函数
salary_stats <- function(x) {
c(
mean = mean(x),
sd = sd(x),
cv = sd(x) / mean(x) # 变异系数
)
}
# 应用自定义函数
group_data %>%
group_by(department) %>%
summarise(
stats = list(salary_stats(salary))
) %>%
unnest_wider(stats)
## group_by: one grouping variable (department)
## summarise: now 3 rows and 2 columns, ungrouped
## # A tibble: 3 × 4
## department mean sd cv
## <chr> <dbl> <dbl> <dbl>
## 1 技术 8875 854. 0.0962
## 2 财务 7250 354. 0.0488
## 3 销售 5050 420. 0.0832
# 使用group_modify()应用函数
group_data %>%
group_by(department) %>%
group_modify(~ {
.x %>%
summarise(
avg = mean(salary),
max = max(salary)
)
})
## group_by: one grouping variable (department)
## summarise: now one row and 2 columns, ungrouped
## summarise: now one row and 2 columns, ungrouped
## summarise: now one row and 2 columns, ungrouped
## # A tibble: 3 × 3
## # Groups: department [3]
## department avg max
## <chr> <dbl> <dbl>
## 1 技术 8875 10000
## 2 财务 7250 7500
## 3 销售 5050 5500
小结:自定义函数可以实现复杂的分组计算。
| 操作 | 函数 | 说明 |
|---|---|---|
| 分组 | group_by() |
创建分组 |
| 取消分组 | ungroup() |
取消分组 |
| 组内筛选 | filter() |
组内条件筛选 |
| 组内排名 | rank()、row_number() |
组内排名 |
| 组内比例 | prop.table() |
组内比例 |
| 组内累计 | cumsum() |
组内累计 |
数据采样和分割是机器学习中的重要步骤。本章介绍各种采样和分割方法。
从数据中随机抽取样本。
# 创建示例数据
sample_data <- data.frame(
id = 1:20,
value = rnorm(20)
)
# 设置随机种子
set.seed(123)
# sample()随机抽样
indices <- sample(1:nrow(sample_data), size = 10, replace = FALSE)
sample_data[indices, ]
## id value
## 15 15 -0.33491276
## 19 19 -0.14539355
## 14 14 -0.21586539
## 3 3 1.69018435
## 10 10 0.82553984
## 2 2 -1.72727063
## 6 6 0.54909674
## 11 11 -0.05568601
## 5 5 2.52833655
## 4 4 0.50381245
# 使用dplyr的slice_sample()
library(dplyr)
slice_sample(sample_data, n = 10)
## slice_sample: removed 10 rows (50%), 10 rows remaining
## id value
## 1 14 -0.21586539
## 2 5 2.52833655
## 3 9 1.29476325
## 4 3 1.69018435
## 5 8 -1.04889314
## 6 10 0.82553984
## 7 7 0.23821292
## 8 15 -0.33491276
## 9 18 1.07061054
## 10 17 -0.08542326
slice_sample(sample_data, prop = 0.5) # 抽取50%
## slice_sample: removed 10 rows (50%), 10 rows remaining
## id value
## 1 4 0.50381245
## 2 14 -0.21586539
## 3 17 -0.08542326
## 4 11 -0.05568601
## 5 7 0.23821292
## 6 5 2.52833655
## 7 12 -0.78438222
## 8 10 0.82553984
## 9 16 -1.08569914
## 10 9 1.29476325
小结:sample()是R内置函数,slice_sample()是dplyr的现代函数。
按比例从各组抽取样本。
# 创建分组数据
strata_data <- data.frame(
group = rep(c("A", "B", "C"), each = 10),
value = rnorm(30)
)
# 分层抽样:每组抽取3个
strata_data %>%
group_by(group) %>%
slice_sample(n = 3)
## group_by: one grouping variable (group)
## slice_sample (grouped): removed 21 rows (70%), 9 rows remaining (removed 0 groups, 3 groups remaining)
## # A tibble: 9 × 2
## # Groups: group [3]
## group value
## <chr> <dbl>
## 1 A -0.295
## 2 A 1.25
## 3 A -1.69
## 4 B -0.695
## 5 B -0.208
## 6 B -0.306
## 7 C -1.12
## 8 C -0.0834
## 9 C -0.0429
# 分层抽样:每组抽取30%
strata_data %>%
group_by(group) %>%
slice_sample(prop = 0.3)
## group_by: one grouping variable (group)
## slice_sample (grouped): removed 21 rows (70%), 9 rows remaining (removed 0 groups, 3 groups remaining)
## # A tibble: 9 × 2
## # Groups: group [3]
## group value
## <chr> <dbl>
## 1 A 1.25
## 2 A -1.14
## 3 A -0.625
## 4 B -0.0619
## 5 B -1.27
## 6 B -0.695
## 7 C -1.12
## 8 C 1.21
## 9 C -0.0285
小结:分层抽样确保每个组都有代表性样本。
将数据划分为训练集和测试集。
# 创建示例数据
ml_data <- data.frame(
id = 1:100,
x = rnorm(100),
y = rnorm(100),
label = sample(c(0, 1), 100, replace = TRUE)
)
# 方法1:随机划分
set.seed(123)
train_indices <- sample(1:nrow(ml_data), size = 0.7 * nrow(ml_data))
train_set <- ml_data[train_indices, ]
test_set <- ml_data[-train_indices, ]
# 方法2:使用caTools包
# 如果没有安装,先运行:install.packages("caTools")
library(caTools)
set.seed(123)
split <- sample.split(ml_data$label, SplitRatio = 0.7)
train_set <- subset(ml_data, split == TRUE)
test_set <- subset(ml_data, split == FALSE)
# 方法3:使用rsample包
# 如果没有安装,先运行:install.packages("rsample")
library(rsample)
set.seed(123)
data_split <- initial_split(ml_data, prop = 0.7)
train_set <- training(data_split)
test_set <- testing(data_split)
小结:rsample包提供了更现代的数据分割方法。
创建交叉验证数据集。
# 使用caret包创建交叉验证折
library(caret)
set.seed(123)
folds <- createFolds(ml_data$label, k = 5) # 5折交叉验证
# 查看每折的索引
str(folds)
## List of 5
## $ Fold1: int [1:20] 1 3 16 20 23 28 35 37 46 49 ...
## $ Fold2: int [1:20] 5 6 9 13 15 17 19 25 27 30 ...
## $ Fold3: int [1:20] 8 21 22 29 32 36 41 43 44 45 ...
## $ Fold4: int [1:20] 2 4 12 14 26 34 38 39 47 58 ...
## $ Fold5: int [1:20] 7 10 11 18 24 31 33 40 48 53 ...
# 使用rsample包
library(rsample)
set.seed(123)
cv_folds <- vfold_cv(ml_data, v = 5) # 5折交叉验证
cv_folds
## # 5-fold cross-validation
## # A tibble: 5 × 2
## splits id
## <list> <chr>
## 1 <split [80/20]> Fold1
## 2 <split [80/20]> Fold2
## 3 <split [80/20]> Fold3
## 4 <split [80/20]> Fold4
## 5 <split [80/20]> Fold5
# 查看每折的训练集和测试集
cv_folds$splits[[1]]
## <Analysis/Assess/Total>
## <80/20/100>
小结:交叉验证是评估模型性能的重要方法。
Bootstrap重采样。
# 使用rsample包进行bootstrap
library(rsample)
set.seed(123)
boot_samples <- bootstraps(ml_data, times = 100) # 100次bootstrap
boot_samples
## # Bootstrap sampling
## # A tibble: 100 × 2
## splits id
## <list> <chr>
## 1 <split [100/41]> Bootstrap001
## 2 <split [100/38]> Bootstrap002
## 3 <split [100/42]> Bootstrap003
## 4 <split [100/40]> Bootstrap004
## 5 <split [100/30]> Bootstrap005
## 6 <split [100/30]> Bootstrap006
## 7 <split [100/36]> Bootstrap007
## 8 <split [100/35]> Bootstrap008
## 9 <split [100/32]> Bootstrap009
## 10 <split [100/36]> Bootstrap010
## # ℹ 90 more rows
# 查看单个bootstrap样本
boot_samples$splits[[1]]
## <Analysis/Assess/Total>
## <100/41/100>
analysis(boot_samples$splits[[1]]) # bootstrap样本
## id x y label
## 1 31 -0.083988713 -0.707607151 1
## 2 79 -1.277450766 -1.472158964 0
## 3 51 -0.260725893 0.286289929 0
## 4 14 -0.374800093 -0.867566116 1
## 5 67 -0.392553554 0.499276876 1
## 6 42 -0.230262235 -0.462879040 0
## 7 50 -1.567675132 2.708168284 1
## 8 43 1.397426653 -1.911151255 1
## 9 14 -0.374800093 -0.867566116 1
## 10 25 -0.627951658 0.736194775 1
## 11 90 -1.595484898 -0.264595238 1
## 12 91 0.851709321 -0.947298279 0
## 13 69 -2.494835444 0.923129239 0
## 14 91 0.851709321 -0.947298279 0
## 15 57 1.871418005 -1.049021530 1
## 16 92 -0.713560806 0.739521328 0
## 17 9 -0.006198262 1.844838671 1
## 18 93 1.066430344 0.896778709 0
## 19 99 -0.564269541 0.489080318 1
## 20 72 -0.084140473 -3.310678727 0
## 21 26 -0.046916726 1.365776554 0
## 22 7 -1.427685784 -0.283702696 0
## 23 42 -0.230262235 -0.462879040 0
## 24 9 -0.006198262 1.844838671 1
## 25 83 0.837455146 0.795805155 0
## 26 36 1.176220124 -0.126160381 1
## 27 78 0.440692091 0.971703576 1
## 28 81 0.902505834 2.454400353 1
## 29 43 1.397426653 -1.911151255 1
## 30 76 -0.263992919 -1.196351747 0
## 31 15 -0.319393809 -0.501187592 0
## 32 32 0.410363449 -1.275543372 1
## 33 7 -1.427685784 -0.283702696 0
## 34 9 -0.006198262 1.844838671 1
## 35 41 0.526855669 -0.870464442 1
## 36 74 -1.093413799 0.892485867 0
## 37 23 0.206372695 1.900900074 1
## 38 27 0.162618115 -0.576263893 1
## 39 60 -0.620326487 -1.570135523 0
## 40 53 0.853895464 -0.989678019 1
## 41 7 -1.427685784 -0.283702696 0
## 42 53 0.853895464 -0.989678019 1
## 43 27 0.162618115 -0.576263893 1
## 44 96 -1.828626634 0.464941753 0
## 45 38 -0.945617940 0.469902192 1
## 46 89 -0.088685452 -1.388245289 1
## 47 34 1.778741618 -1.093589748 1
## 48 93 1.066430344 0.896778709 0
## 49 69 -2.494835444 0.923129239 0
## 50 72 -0.084140473 -3.310678727 0
## 51 76 -0.263992919 -1.196351747 0
## 52 63 1.117486481 -1.151603163 0
## 53 13 -0.778997240 0.975142936 1
## 54 82 -1.261304177 -1.649345480 1
## 55 97 -1.813533356 -1.951030610 1
## 56 91 0.851709321 -0.947298279 0
## 57 25 -0.627951658 0.736194775 1
## 58 38 -0.945617940 0.469902192 1
## 59 21 0.300279118 1.797140441 0
## 60 79 -1.277450766 -1.472158964 0
## 61 41 0.526855669 -0.870464442 1
## 62 47 0.151611371 -0.284001390 1
## 63 90 -1.595484898 -0.264595238 1
## 64 60 -0.620326487 -1.570135523 0
## 65 95 0.535917056 -1.782057068 0
## 66 16 0.084543768 0.785591163 1
## 67 94 -0.536242590 -0.346000887 0
## 68 6 -0.735026156 -0.331284485 0
## 69 72 -0.084140473 -3.310678727 0
## 70 86 -0.047867742 -0.922960815 1
## 71 86 -0.047867742 -0.922960815 1
## 72 39 -0.665188639 0.960382357 0
## 73 31 -0.083988713 -0.707607151 1
## 74 81 0.902505834 2.454400353 1
## 75 50 -1.567675132 2.708168284 1
## 76 34 1.778741618 -1.093589748 1
## 77 4 -0.228895815 -1.177967499 1
## 78 13 -0.778997240 0.975142936 1
## 79 69 -2.494835444 0.923129239 0
## 80 25 -0.627951658 0.736194775 1
## 81 52 0.961810352 -0.032510251 0
## 82 22 0.074856824 -0.810326258 1
## 83 89 -0.088685452 -1.388245289 1
## 84 32 0.410363449 -1.275543372 1
## 85 25 -0.627951658 0.736194775 1
## 86 87 -2.399197708 0.009898130 1
## 87 35 0.037682847 0.192441582 0
## 88 40 0.452030192 -0.005141288 0
## 89 30 0.305463227 0.791547909 1
## 90 12 -0.782730275 -0.204667665 0
## 91 31 -0.083988713 -0.707607151 1
## 92 30 0.305463227 0.791547909 1
## 93 64 1.184930635 -0.160788483 1
## 94 99 -0.564269541 0.489080318 1
## 95 14 -0.374800093 -0.867566116 1
## 96 93 1.066430344 0.896778709 0
## 97 96 -1.828626634 0.464941753 0
## 98 71 0.628568823 -0.139500325 0
## 99 67 -0.392553554 0.499276876 1
## 100 23 0.206372695 1.900900074 1
assessment(boot_samples$splits[[1]]) # 未被抽中的样本(OOB)
## id x y label
## 1 1 0.412022275 -0.01863398 1
## 2 2 0.793585308 0.36237035 1
## 3 3 -0.152410633 2.01130559 1
## 4 5 -0.900791751 -0.75517048 0
## 5 8 0.619283535 0.31415290 1
## 6 10 -0.685706846 -0.98191715 1
## 7 11 -0.279333528 2.19600376 0
## 8 17 -0.768473603 -2.10224732 0
## 9 18 -0.625910913 -0.04220493 1
## 10 19 -0.900870855 -0.40480941 1
## 11 20 0.663728670 -0.11276597 1
## 12 24 -0.488922835 0.70895444 1
## 13 28 1.292305915 -0.80473232 1
## 14 29 -0.463556502 -0.53506581 0
## 15 33 0.183678241 2.37546446 1
## 16 37 -0.558535810 -1.38843214 0
## 17 44 1.763653031 0.36981604 1
## 18 45 0.485601354 -0.46228806 0
## 19 46 -0.265738944 0.32195589 0
## 20 48 1.376609813 1.09185644 0
## 21 49 -0.180394314 0.16021636 0
## 22 54 0.418796702 0.21180993 1
## 23 55 0.339956513 -0.97201753 0
## 24 56 0.596425059 0.54922401 0
## 25 58 0.602870370 0.44913393 0
## 26 59 -0.766168804 -0.38585565 1
## 27 61 0.790190251 1.22694260 1
## 28 62 -0.241976510 0.66669440 0
## 29 65 1.646474838 -0.97291378 1
## 30 66 0.192996186 -0.61046166 1
## 31 68 0.007051323 0.87936414 1
## 32 70 -0.977296362 0.69313397 0
## 33 73 0.382131608 0.53846580 1
## 34 75 -0.755463961 0.48946204 1
## 35 77 -0.752462697 0.46273785 0
## 36 80 1.177192046 0.52896494 1
## 37 84 -2.348290308 -0.34129317 1
## 38 85 0.610971137 -1.40525380 1
## 39 88 -0.019318956 -0.40885977 1
## 40 98 1.372613707 -0.51611334 0
## 41 100 0.970311227 0.90202473 0
小结:Bootstrap重采样用于估计统计量的分布。
| 操作 | 函数/包 | 说明 |
|---|---|---|
| 随机抽样 | sample()、slice_sample() |
随机抽取样本 |
| 分层抽样 | group_by() + slice_sample() |
按组抽样 |
| 数据划分 | caTools、rsample |
训练/测试集划分 |
| 交叉验证 | createFolds()、vfold_cv() |
创建CV折 |
| Bootstrap | bootstraps() |
重采样 |
本章介绍数据清洗的实战技巧和最佳实践。
使用管道操作符使代码更清晰。
# 创建示例数据
raw_data <- data.frame(
id = c(1, 2, 3, 4, 5),
name = c("张三 ", " 李四", "王五", "赵六", "钱七"),
age = c(25, 30, NA, 35, 40),
salary = c(5000, 6000, 7000, NA, 9000)
)
# 使用管道进行数据清洗
clean_data <- raw_data %>%
# 去除姓名中的空格
mutate(name = trimws(name)) %>%
# 用均值填充缺失值
mutate(
age = ifelse(is.na(age), mean(age, na.rm = TRUE), age),
salary = ifelse(is.na(salary), mean(salary, na.rm = TRUE), salary)
) %>%
# 添加新列
mutate(salary_level = ifelse(salary > 6000, "高", "低"))
## mutate: changed 2 values (40%) of 'name' (0 new NAs)
## mutate: changed one value (20%) of 'age' (one fewer NA)
## changed one value (20%) of 'salary' (one fewer NA)
## mutate: new variable 'salary_level' (character) with 2 unique values and 0% NA
clean_data
## id name age salary salary_level
## 1 1 张三 25.0 5000 低
## 2 2 李四 30.0 6000 低
## 3 3 王五 32.5 7000 高
## 4 4 赵六 35.0 6750 高
## 5 5 钱七 40.0 9000 高
# 使用R 4.1+的原生管道符 |>
clean_data_native <- raw_data |>
dplyr::mutate(name = trimws(name))
小结:管道操作使数据处理流程清晰可读。
封装清洗函数实现自动化。
# 定义数据清洗函数
clean_employee_data <- function(data) {
data %>%
# 去除空格
mutate(across(where(is.character), trimws)) %>%
# 填充数值型缺失值
mutate(across(where(is.numeric),
~ ifelse(is.na(.), mean(., na.rm = TRUE), .))) %>%
# 删除全空的行
filter(complete.cases(.))
}
# 应用清洗函数
clean_data <- clean_employee_data(raw_data)
## mutate: changed 2 values (40%) of 'name' (0 new NAs)
## mutate: changed one value (20%) of 'age' (one fewer NA)
## changed one value (20%) of 'salary' (one fewer NA)
## filter: no rows removed
clean_data
## id name age salary
## 1 1 张三 25.0 5000
## 2 2 李四 30.0 6000
## 3 3 王五 32.5 7000
## 4 4 赵六 35.0 6750
## 5 5 钱七 40.0 9000
小结:封装函数可以实现清洗流程的复用。
处理Excel多表格数据。
# 使用readxl包读取Excel
library(readxl)
# 读取单个工作表
data <- read_excel("data.xlsx", sheet = "Sheet1")
# 读取所有工作表
all_sheets <- excel_sheets("data.xlsx")
all_data <- lapply(all_sheets, function(sheet) {
read_excel("data.xlsx", sheet = sheet)
})
names(all_data) <- all_sheets
# 合并所有工作表
combined_data <- bind_rows(all_data, .id = "sheet_name")
小结:readxl包可以方便地处理Excel文件。
使用data.table处理大数据。
# data.table是处理大数据的高效工具
library(data.table)
# 读取大数据
big_data <- fread("large_file.csv")
# data.table的语法
# DT[i, j, by] 表示:[行筛选, 列操作, 分组]
# 示例:按组汇总
result <- big_data[, .(mean_value = mean(value), count = .N), by = group]
# 链式操作
result <- big_data[
value > 0, # 筛选行
.(total = sum(value)), # 计算汇总
by = group # 按组
][order(-total)] # 排序
小结:data.table语法高效,适合处理大数据。
使用tidylog包记录清洗过程。
# tidylog包可以显示dplyr操作的结果统计
# 如果没有安装,先运行:install.packages("tidylog")
library(tidylog)
# 现在dplyr操作会显示详细统计
raw_data %>%
filter(!is.na(age)) %>%
select(id, name, age) %>%
distinct()
# 会显示:filter removed 1 row, select kept 3 columns等
小结:tidylog包帮助追踪数据清洗过程。
使用validate包验证数据质量。
# validate包用于数据验证
# 如果没有安装,先运行:install.packages("validate")
library(validate)
# 定义验证规则
rules <- validator(
age >= 0, # 年龄非负
age <= 120, # 年龄上限
salary > 0, # 薪资为正
!is.na(name) # 姓名不能为空
)
# 执行验证
results <- confront(raw_data, rules)
# 查看验证结果
summary(results)
小结:validate包可以系统地检查数据质量。
检查数据中的冲突和矛盾。
# 创建示例数据(包含一些不一致的记录)
check_data <- data.frame(
id = 1:5,
start_date = as.Date(c("2024-01-01", "2024-02-01", "2024-01-15", "2024-03-01", "2024-02-15")),
end_date = as.Date(c("2024-01-31", "2024-01-15", "2024-02-15", "2024-03-31", "2024-02-01")),
amount = c(100, -50, 200, 150, 300)
)
# 检查日期一致性:结束日期应该大于开始日期
invalid_records <- check_data %>%
mutate(
date_valid = end_date > start_date,
amount_valid = amount >= 0
) %>%
filter(!date_valid | !amount_valid)
## mutate: new variable 'date_valid' (logical) with 2 unique values and 0% NA
## new variable 'amount_valid' (logical) with 2 unique values and 0% NA
## filter: removed 3 rows (60%), 2 rows remaining
# 显示不一致的记录
invalid_records
## id start_date end_date amount date_valid amount_valid
## 1 2 2024-02-01 2024-01-15 -50 FALSE FALSE
## 2 5 2024-02-15 2024-02-01 300 FALSE TRUE
# 使用assertthat包进行断言检查
library(assertthat)
# 先检查是否存在问题记录,再进行断言
if (nrow(invalid_records) > 0) {
message("发现 ", nrow(invalid_records), " 条不一致的记录,需要处理")
}
## 发现 2 条不一致的记录,需要处理
# 正确的断言方式:检查清洗后的数据
clean_check_data <- check_data %>%
filter(end_date > start_date, amount >= 0)
## filter: removed 2 rows (40%), 3 rows remaining
# 对清洗后的数据进行断言
assert_that(nrow(clean_check_data) > 0, msg = "清洗后数据为空")
## [1] TRUE
assert_that(all(clean_check_data$end_date > clean_check_data$start_date),
msg = "存在结束日期早于开始日期的记录")
## [1] TRUE
小结:一致性检查可以发现数据中的逻辑错误。
将清洗后的数据输出到各种格式。
# 输出为CSV
write_csv(clean_data, "clean_data.csv")
# 输出为Excel
library(writexl)
write_xlsx(clean_data, "clean_data.xlsx")
# 输出为RDS(保留R对象结构)
write_rds(clean_data, "clean_data.rds")
# 输出为RData(可保存多个对象)
save(clean_data, raw_data, file = "data.RData")
# 写入数据库
library(DBI)
con <- dbConnect(RSQLite::SQLite(), "database.db")
dbWriteTable(con, "clean_data", clean_data)
dbDisconnect(con)
小结:根据用途选择合适的输出格式。
| 技巧 | 工具 | 说明 |
|---|---|---|
| 管道操作 | %>%、|> |
流程清晰 |
| 自动化 | 自定义函数 | 复用清洗流程 |
| 大数据 | data.table |
高效处理 |
| 日志记录 | tidylog |
追踪过程 |
| 数据验证 | validate |
检查质量 |
| 数据输出 | write_csv等 |
保存结果 |
恭喜你完成了R语言数据清洗与预处理的学习!数据清洗是数据分析的基础,掌握这些技能将使你的分析工作更加高效和专业。
记住数据清洗的核心原则: