一、基本数据类型与数据结构 🔗
R 中最基本的数据类型有三种:数字、文本、逻辑。其中数字和文本又可衍生出几种不同的类型。每个 R 数据对象都可以用class()
函数来查看数据类型。用str()
查看数据内部的基础属性,用 is 类 函数(is.integer()/is.numeric()/is.double()/is.complex()/is.character()/is.factor()/is.logical()
)判断是否是某种数据类型,用 as 类函数(as.integer()/as.numeric()/as.double()/as.complex()/as.character()/as.factor()/as.logical()
)转换数据类型。而日期类型较为复杂一些,这里不展开。
- 数字
- 整型(integer)
- 数值型(numeric),双精度浮点型1(double)
- 复数型(complex)
- 文本
- 字符型(character)
- 因子型(factor)
- 逻辑(logical)
- TRUE/FALSE
- 日期2(Date)
数据需要存放到一起才便于分析,根据存储和处理数据方面的差异,R 中有以下几种数据结构,可以分别用英文单词命名的函数(c()/vector()/matrix()/array()/data.frame()/list()
)来创建不同的数据结构。每个对象可以用str()
函数来查看数据结构,用 is 类函数(is.vector()/is.matrix()/is.array()/is.data.frame()/is.list()
)来判断是否是某种数据结构,用 as 类函数(as.vector()/as.matrix()/as.array()/as.data.frame()/as.list()
)转换成不同的数据结构。
- 向量(vector),一维数组,用于存储同一数据类型的数据。
- 矩阵(matrix),二维数组,用于存储同一数据类型的数据。
- 数组(array),不限维度,用于存储同一数据类型的数据。
- 数据框(data.frame),二维,不同列可以存储不同数据类型,各列长度必须相同。
- 列表(list),可以包含向量、矩阵、数组、数据框。
需要注意的是,数据框和列表可以存储不同数据类型,而向量、矩阵、数组只能存储相同数据类型,因此倘若在创建向量、矩阵或数组时引入了不同数据类型的元素,那么这些元素会被统一转换成相同数据类型。如下数字1、2和字符a、b在组成向量或矩阵时,由于在 R 中数值能转换成字符串而字符串不能转换成数值,所以数字1、2被统一转换成了字符串,而组成数据框时则保留了原本的数据类型。
x <- c(1, 2, 'a', 'b')
str(x)
## chr [1:4] "1" "2" "a" "b"
y <- matrix(c(1, 2, 'a', 'b'), nrow = 2, ncol = 2)
str(y)
## chr [1:2, 1:2] "1" "2" "a" "b"
z <- data.frame(c(1, 2), c('a', 'b'))
str(z)
## 'data.frame': 2 obs. of 2 variables:
## $ c.1..2. : num 1 2
## $ c..a....b..: chr "a" "b"
# 判断两个数据对象是否完全一致,数据结构和元素的数据类型都一致
identical(x, y) ;identical(x, z)
## [1] FALSE
## [1] FALSE
# 判断两个数据对象中元素是否一致
setequal(x, y) ;setequal(x, z)
## [1] TRUE
## [1] FALSE
二、apply 族基本用法 🔗
由于 apply 族本身是 R 中的函数,而 apply 函数内部又可以通过 FUN 参数写入函数,为了避免混淆,将前者统一写作应用 apply 函数,将后者统一写作调用函数。
R 中 apply 族函数有多个,分别针对不同的数据结构或不同的应用场景,其基本功能是对输入的数据对象调用指定函数,被调用的函数是 R 中已有或者自定义的。且重点是 apply 并非是使用循环挨个对每个元素调用函数,而是批量进行,应比执行普通的 for 循环更快 。
函数名 | 函数名首字母及含义 | 输入数据对象的类型 | 调用函数的应用范围 | 是否默认简化输出结果 |
---|---|---|---|---|
apply | 应用 apply 调用函数 | 数组 | 选择维度 | 是 |
lapply | list,对列表调用函数 | 列表 | 所有元素 | 否 |
sapply | simplify,简化输出结果 | 列表 | 所有元素 | 是 |
vapply | validate,验证数据类型 | 列表 | 所有元素 | 否 |
mapply | multiple,多个数据对象 | 列表 | 多个数据对象的所有元素 | 是 |
tapply | table,分组计算 | 向量、数据框 | 向量 | 是 |
rapply | recursive,递归 | 列表 | 嵌套列表中所有元素 | 是 |
eapply | environment,环境 | 列表 | 环境中所有数据对象 | 否 |
为便于后续复现,创建一些基础的数据对象。
# 创建一个维度为(2, 3, 4)的三维数组
my_array <- array(c(1:24), dim = c(2, 3, 4))
# 取 my_array中的第1个矩阵
# 由于默认按列填充数据,所以第一行是 1 3 5 而不是 1 2 3
my_matrix <- my_array[, , 1]
my_vector <- my_matrix[1,]
my_list <- list(my_array, my_matrix, my_vector)
seed <- 2024
my_dataframe <-
data.frame(value = c(1:24),
type = sample(c('A', 'B', 'C', 'D'), replace = TRUE))
2.1.apply 🔗
apply 函数中涉及的参数用法大致如下。
apply(X, MARGIN, FUN, ..., simplify = TRUE)
-
X 参数,输入数据对象,应至少是2维的。
-
MARGIN 参数,用于指定按什么维度调用函数。通常情况下,填入1代表选择行,填入2代表选择列,填入
c(1, 2)
或者1:2
代表选择所有行和列。由于 apply 函数也可应用于多维数组,因此对于一个 N 维数组来说此参数的范围是1到 N,当填入MARGIN = N
时,表示分别对 N 个 N-1维的数组进行计算。 -
FUN 参数,用于指定具体调用的函数,可以是 R 中的基础函数,如求和(sum)、求平均值(mean)、求开方(sqrt)、求分位数(quantile)、排序(sort)等,或者可以自定义函数。
-
...
,通常放在 FUN 参数后面,用于设置所调用函数的参数细节。比如apply(X, MARGIN, FUN = quantile, probs = 1:3/4)
,相当于指定仅计算25%、50%、75%等三个分位数值。 -
simplify 参数,默认取值为 TRUE,即默认将输出结果简化为向量、矩阵。当设置
simplify = "array"
,输出结果会是更高维度的数组。
需要指出的是,应用 apply 函数输出结果的维度设定如下,这里一时看不懂也没关系,后来跑跑代码、看看输出结果自然就会明白了。
如果每次调用 FUN 返回的向量长度一致且为 n ,并且默认 simplify 为 TRUE,则返回一个维度为
c(n, dim(X)[MARGIN])
的数组(如果 n > 1)。如果 n 等于 1,则返回一个向量,且如果 MARGIN 的长度为 1,则返回维度为dim(X)[MARGIN]
的数组。 如果 n 为 0,则结果的长度为 0,但不一定是“正确”的维度。
如果每次调用 FUN 返回的向量长度不一致,或者 simplify 为 FALSE,输出结果的维度参照 MARGIN 参数, 返回长度为 prod(dim(X)[MARGIN]) 的列表。
2.1.1.调用统计函数 🔗
当数据框中各列数据类型一致时,可等同于二维数组,因而在一些特殊情况下也可以对数据框使用 apply 函数。本节中的数据框 my_dataframe
有两列,value 列为数值型,type 列为字符型。当 FUN 参数填入的函数只能对类型一致的数据进行计算时,无论按行或按列都会报错,比如求和(sum);而填入对数据类型没有要求的函数时,那么可以计算出结果,比如求最大(max)、判断缺失(is.na
)等。
# 按列求和,由于无法对数值和字符串进行求和,会报错
# apply(X = my_dataframe, MARGIN = 2, FUN = sum)
# 按列求最大值
apply(X = my_dataframe, MARGIN = 2, FUN = max)
## value type
## "24" "D"
当 FUN 参数输入的是带有汇总性质的统计函数时,如果 MARGIN 参数仅选择一个维度,那么输出的将会是按该维度的汇总结果。
bak <- my_matrix
dimnames(bak)[[1]] <- c('第1行', '第2行')
dimnames(bak)[[2]] <- c('第1列', '第2列', '第3列')
# 按列求和
col.sums <- apply(X = bak, 2, sum)
# 按行求和
row.sums <- apply(X = bak, 1, sum)
# 汇总按行与列的和
rbind(cbind(bak, "行汇总" = row.sums), "列汇总" = c(col.sums, sum(col.sums)))
## 第1列 第2列 第3列 行汇总
## 第1行 1 3 5 9
## 第2行 2 4 6 12
## 列汇总 3 7 11 21
2.1.2.调用对元素进行操作的函数 🔗
当 FUN 参数输入的是针对每个元素进行转换的函数时,如果 MARGIN 参数还是仅选择一个维度,那么可能会得到“奇怪”的效果。
如下例子输入的函数是让 x 成为 x 自己,若选择的维度是1,代表按行应用函数。X 输入的是一个2行3列的矩阵,得到的结果是一个3行2列的矩阵,相当于应用 apply 后的每一行是应用前的每一列,就成了转置的效果。
apply(X = my_matrix, MARGIN = 1, FUN = function(x) x)
## [,1] [,2]
## [1,] 1 2
## [2,] 3 4
## [3,] 5 6
若要解释“转置”的由来,需要先知道应用 apply 函数时默认会简化结果,即simplify = TRUE
。那么下面看看当设置成不简化结果时,输出的结果如下。即在输入的2行3列矩阵的基础上,分别按行取出了2个向量,并由此组成了一个列表。于是在默认简化结果时,这两个向量(1, 3, 5)
和(2, 4, 6)
以列向量(1列3行)的形式组成了一个2列3行的矩阵。
apply(X = my_matrix, MARGIN = 1, FUN = function(x) x, simplify = FALSE)
## [[1]]
## [1] 1 3 5
##
## [[2]]
## [1] 2 4 6
同样是让 x 成为 x 自己,若选择的维度是2,即代表按列应用函数。如下特别设定不简化结果,可以看到输入的2行3列矩阵的基础上,分别按列取出了3个向量。
apply(X = my_matrix, MARGIN = 2, FUN = function(x) x, simplify = FALSE)
## [[1]]
## [1] 1 2
##
## [[2]]
## [1] 3 4
##
## [[3]]
## [1] 5 6
在默认简化结果时,取出的3个向量(1, 2)
、(3, 4)
、(5, 6)
以列向量(1列2行)的形式组成了一个3列2行的矩阵,即原矩阵。
apply(X = my_matrix, MARGIN = 2, FUN = function(x) x)
## [,1] [,2] [,3]
## [1,] 1 3 5
## [2,] 2 4 6
2.1.3.应用于多维数组 🔗
如下在一个dim = c(2, 3, 4)
的多维数组上,选择的维度为1,即按行应用求和函数。输入的数组只有2行,于是得到2个求和结果。
apply(X = my_array, MARGIN = 1, FUN = sum)
## [1] 144 156
第1行的求和结果是144,若是好奇144是哪些数字的和,可以打印多维数组的第一行看看。
print(my_array[1,,])
## [,1] [,2] [,3] [,4]
## [1,] 1 7 13 19
## [2,] 3 9 15 21
## [3,] 5 11 17 23
如若是像上一小节调用函数让 x 成为 x 自己,会得到一个12行两列的矩阵。apply 函数内部的运算过程可以这样理解:按指定维度取出子矩阵(数组)–>用as.vector()
使子矩阵变成向量–>以列向量的形式组成最后结果矩阵。
- 从输入的三维数组中分别按行取出子矩阵,即
my_array[1,,]
和my_array[2,,]
这两个3行4列的矩阵。 - 按照 apply 函数的特性——默认简化结果,子矩阵被
as.vector()
函数处理成向量,那么如my_array[1,,]
这个3行4列的矩阵,经过as.vector(my_array[1,,])
会变成一个列向量(12行1列)。 - 最后输出结果的维度为
c(12, dim(my_array)[1])
,即两个列向量(12行1列)组成一个12行2列的矩阵。
apply(X = my_array, MARGIN = 1, FUN = function(x) x)
## [,1] [,2]
## [1,] 1 2
## [2,] 3 4
## [3,] 5 6
## [4,] 7 8
## [5,] 9 10
## [6,] 11 12
## [7,] 13 14
## [8,] 15 16
## [9,] 17 18
## [10,] 19 20
## [11,] 21 22
## [12,] 23 24
2.1.4.应用于多个维度 🔗
如下对维度为2x3x4的数组调用函数让 x 成为 x 自己,最终输出结果是3个4x2的矩阵,即维度为4x2x3的数组。这个过程可以这样理解。
- 从输入的三维数组中分别按第一维度(行)和第二维度(列)取出6个子向量,即
my_array[1, 1, ]
、my_array[2, 1, ]
、my_array[1, 2, ]
、my_array[2, 2, ]
、my_array[1, 3, ]
、my_array[2, 3, ]
等6个长度为4的向量。 - 子向量被
as.vector()
函数处理后依然是向量。 - 最后输出结果维度为
c(4,dim(my_array)[1:2])
,4x2x3,即6个长度为4的列向量(4行1列)组成了3个4行2列的矩阵。
apply(X = my_array, MARGIN = 1:2, FUN = function(x) x)
## , , 1
##
## [,1] [,2]
## [1,] 1 2
## [2,] 7 8
## [3,] 13 14
## [4,] 19 20
##
## , , 2
##
## [,1] [,2]
## [1,] 3 4
## [2,] 9 10
## [3,] 15 16
## [4,] 21 22
##
## , , 3
##
## [,1] [,2]
## [1,] 5 6
## [2,] 11 12
## [3,] 17 18
## [4,] 23 24
2.2.lapply / sapply 🔗
lapply 或 sapply 与 apply 函数的区别有这几点:
-
apply 可输入的数据对象仅限于数组,而 lapply 或 sapply 可输入任意数据对象( R 中任何数据对象都可以看做是一个列表)。
-
apply 指定 MARGIN 参数来选择对输入对象中的某一或某几个维度调用函数,而 lapply 或 sapply 对输入数据对象中的每个元素调用函数。
lapply(X, FUN, ...)
sapply(X, FUN, ..., simplify = TRUE)
lapply 与 apply 之间的区别是,lapply 输出结果只会是一个列表,而 sapply 在 lapply 的基础上默认将输出结果简化为向量或矩阵。如果使用 lapply 时输出列表中的所有元素都是相同的类型,则使用 sapply 时 输出结果将简化为一个向量,否则就输出一个矩阵。
如下例子所示,对一个数组使用 lapply 时,会对原数组中每个维度下的每个元素调用函数,并且将作为列表中的元素输出;而对一个数组使用 sapply 时,由于输出的每个元素都是数值型数据,于是简化成输出一个数值型向量。
# 输出列表
#lapply(X = my_array, FUN = function(x) x)
# 输出向量
sapply(X = my_array, FUN = function(x) x)
## [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
再如下例子所示,对一个数据框使用 lapply 时,会对数据框中每一列调用函数,将每一列的结果作为一个列表中的元素输出。;而对一个数据框使用 sapply 时,如果输出的各列数据类型不一致,会自动转换成统一的数据类型,且输出时简化为一个向量。
lapply(X = my_dataframe, FUN = max)
## $value
## [1] 24
##
## $type
## [1] "D"
sapply(X = my_dataframe, FUN = max)
## value type
## "24" "D"
2.3.lapply -> vapply 🔗
与 lapply 相比,vapply 函数需要设定 FUN.VALUE 参数,用于指定调用函数后返回结果的数据类型和结构(长度),以此验证输出结果与设定的一致。下面例子中输入的数据对象是一个数据框,其中一列为数值型,另一列为字符型,不论设定 FUN.VALUE 参数为长度为1的数字型还是字符型,结果都会报错。这也说明此例中 FUN.VALUE 参数是分别对 FUN(X[[1]])
和 FUN(X[[2]])
的结果进行验证的。
vapply(X = my_dataframe, FUN = max, FUN.VALUE = numeric(1))
Error in vapply(my_dataframe, FUN = max, FUN.VALUE = numeric(1)) : values must be type ‘double’, but FUN(X[[2]]) result is type ‘character’
vapply(X = my_dataframe, FUN = max, FUN.VALUE = character(1))
Error in vapply(my_dataframe, FUN = max, FUN.VALUE = character(1)) : values must be type ‘character’, but FUN(X[[1]]) result is type ‘integer’
2.4.sapply -> mapply 🔗
与 sapply 相比,mapply 可以对多个数据对象调用函数,并且可以在调用函数时设定多个参数。需要注意的是使用 mapply 所填入的参数顺序,调用的函数写在最前面。
sapply(X = 列表1, FUN = 调用函数名称)
mapply(FUN = 调用函数名称, 列表1, 列表2, ..., 列表n, MoreArgs = list(调用函数参数1, 调用函数参数2, ..., 调用函数参数n))
mapply 对多个数据对象调用函数时,并不是单独对每个数据对象调用函数。如下对my_matrix[1, ]
和my_matrix[2, ]
调用 sum 函数,得到的结果并不是sum(my_matrix[1, ])
和sum(my_matrix[2, ])
,而是对两个长度为3的向量中每组一一对应的元素来求和,这才有1+2=3
、3+4=7
、5+6=11
,其实就相当于my_matrix[1, ] + my_matrix[2, ]
。
mapply(FUN = sum, my_matrix[1, ], my_matrix[2, ])
## [1] 3 7 11
而当 mapply 对多个不同类型的数据对象调用函数时,如下对一个向量和一个矩阵调用 sum 函数,基于 apply 默认简化结果的特点,最终输出的相当于as.vector(my_matrix[1,] + my_matrix)
。
mapply(FUN = sum, my_matrix[1, ], my_matrix)
## [1] 2 5 8 5 8 11
如果 mapply 中输入了多个数据对象,而调用的函数只能输入一个参数,比如让 x 成为 x 自己,这样会报错。
mapply(FUN = function(x) x, my_matrix[1, ], my_matrix[2, ])
Error in (function (x) : unused argument (dots[[2]][[1]])
2.5.sapply -> tapply 🔗
与 sapply 相比,tapply 可以做分组计算,其中用 INDEX 参数来指定一个或多个分组的依据。
tapply(X, INDEX, FUN = NULL, ..., default = NA, simplify = TRUE)
如下,用 apply 对数据框调用 summary 函数,这里相当于summary(my_dataframe)
,即输出数据框中每一列的摘要信息。
sapply(X = my_dataframe, FUN = summary)
## $value
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1.00 6.75 12.50 12.50 18.25 24.00
##
## $type
## Length Class Mode
## 24 character character
而用 tapply 对数据框调用 summary 函数,可以指定按分组依据来计算各个组别的摘要信息。
tapply(X = my_dataframe$value, INDEX = my_dataframe$type, FUN = summary)
## $A
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1 6 11 11 16 21
##
## $B
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 3 8 13 13 18 23
##
## $D
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 2.0 7.5 13.0 13.0 18.5 24.0
tapply 也可以指定多个分组依据,如下。
my_dataframe$new_type <- sample(c('甲', '乙', '丙', '丁'), replace = TRUE)
tapply(
X = my_dataframe$value,
INDEX = my_dataframe[, -1],
FUN = sum,
default = 0 # 缺失值填0
)
## new_type
## type 丁 甲 乙
## A 66 0 0
## B 0 0 78
## D 0 84 72
值得一提的是 tapply 中 FUN 参数可以输入 NULL 值,此时相当于将对输入的数据对象进行分组但不做任何计算,输出的结果是数据对象中每个元素对于的组别序号。
tapply(X = my_dataframe$value, INDEX = my_dataframe$type, FUN = NULL)
## [1] 1 3 2 3 1 3 2 3 1 3 2 3 1 3 2 3 1 3 2 3 1 3 2 3
2.6.lapply -> eapply 🔗
与 lapply 相比,eapply 可以对环境中的多个数据对象调用函数,且输出时不简化结果。
eapply(env, FUN, ..., all.names = FALSE, USE.NAMES = TRUE)
如下是一个简单例子,用 eapply 对环境中的数据对象调用 sum 函数。
# 创建一个新的环境对象,不使用哈希表(耗内存但更快),使用列表的形式存储数据对象
env <- new.env(hash = FALSE)
env$my_matrix <- my_matrix
env$my_vector <- my_vector
tmp1 <- eapply(env, FUN = sum)
tmp1
## $my_vector
## [1] 9
##
## $my_matrix
## [1] 21
创建新的环境对象相当于创建一个存储数据的容器,如果选择用列表的方式来存储,等同于将各个数据对象组成一个新的列表,所以在下面这种情况下用 eapply 和 lapply 的输出是一致的。
tmp2 <-
lapply(
X = list(my_vector = my_vector, my_matrix = my_matrix),
FUN = sum
)
identical(tmp1, tmp2)
## [1] TRUE
2.7.lapply -> rapply 🔗
与 lapply 相比,rapply 可以对嵌套列表中每层列表的每个元素调用函数。其中 r(recursive)代表的含义是递归,但仅仅只是向下递归。
rapply(object, f, classes = "ANY", deflt = NULL, how = c("unlist", "replace", "list"), ...)
如下所示,用 lapply 对一个嵌套列表调用函数求平方,执行后会报错。这是因为 lapply 是对输入列表中的每个元素调用函数,相当于分别对第一个元素1、第二个元素list(2, 3)
求平方,而求平凡这个函数无法对一个列表生效,于是就报错了。
lapply(X = list(1, list(2, 3)), FUN = function(x) {x^2})
Error in x^2 : non-numeric argument to binary operator
改为用 rapply 时,尽管第一层列表中的第二个元素list(2, 3)
也是一个列表,但仍然会对嵌套的列表中的每个元素调用函数,于是能输出结果。
rapply(object = list(1, list(2, 3)), f = function(x) {x^2})
## [1] 1 4 9
rapply 默认设置classes = "ANY"
,即对嵌套列表中的任何类型的数据都调用函数。当在 rapply 输入的数据对象是一个包含多种数据类型的嵌套列表时,也可以指定只对一种数据类型调用函数。比如下面,只对数值计算求平方,或只对字符转换成大写。
rapply(
object = list(1, list(2, 3, list('a', 'b'))),
f = function(x) {
x ^ 2
},
class = c("numeric")
)
## [1] 1 4 9
rapply(
object = list(1, list(2, 3, list('a', 'b'))),
f = toupper,
class = c("character")
)
## [1] "A" "B"
rapply 也默认设置how = "unlist"
,即将输出结果简化为向量。此外输出结果还可以设定成另两种方式。其一是how = "list"
,输出结果与输入的数据对象结构一致,但每个元素会被替换为调用函数后的结果,此时可设置 deflt 参数来填补缺失;其二是how = "replace"
,与前者的区别是,仅有调用了函数的部分会被替换,而没调用过函数的部分仍然与输入时保持不变。
rapply(
object = list(1, list(2, 3, list('a', 'b'))),
f = toupper,
class = c("character"),
how = "list",
deflt = "填补" #设置 deflt 参数填补 NULL
)
## [[1]]
## [1] "填补"
##
## [[2]]
## [[2]][[1]]
## [1] "填补"
##
## [[2]][[2]]
## [1] "填补"
##
## [[2]][[3]]
## [[2]][[3]][[1]]
## [1] "A"
##
## [[2]][[3]][[2]]
## [1] "B"
rapply(
object = list(1, list(2, 3, list('a', 'b'))),
f = toupper,
class = c("character"),
how = "replace"
)
## [[1]]
## [1] 1
##
## [[2]]
## [[2]][[1]]
## [1] 2
##
## [[2]][[2]]
## [1] 3
##
## [[2]][[3]]
## [[2]][[3]][[1]]
## [1] "A"
##
## [[2]][[3]][[2]]
## [1] "B"
2.8.aggregate 🔗
aggregate 用于对数据进行分组汇总统计,与 apply 族很相似。由于 R 的开发者们各有各的倔强,整个 apply 族在不同应用场景下的函数名称各不相同,而 aggregate 在不同应用场景下函数名称保持统一。
aggregate.data.frame(x, by, FUN, ..., simplify = TRUE, drop = TRUE)
aggregate.formula(x, data, FUN, ..., subset, na.action = na.omit)
aggregate.ts(x, nfrequency = 1, FUN = sum, ndeltat = 1, ts.eps = getOption("ts.eps"), ...)
2.8.1.aggregate.data.frame 🔗
当 aggregate 作为 aggregate.data.frame 来使用时,作用与 tapply 相似,但如果指定多个分组依据,tapply 的输出结果会排布成列联表的形式,而 aggregate 的输出结果按列的顺序排布如下。
aggregate(
x = my_dataframe$value,
by = list(type = my_dataframe$type, new_type = my_dataframe$new_type),
FUN = sum)
## type new_type x
## 1 A 丁 66
## 2 D 甲 84
## 3 B 乙 78
## 4 D 乙 72
由于 aggregate 是以输入列表的形式来指定分组依据,所以在by = list()
里也可以在原始组别的基础上做进一步划分。另外,tapply 在 INDEX 参数里指定分组依据时也可以做到这点,但似乎只能在只有一个分组依据时实现。
aggregate(
x = my_dataframe$value,
by = list(type = my_dataframe$type, new_type = my_dataframe[, "new_type"] %in%
c('甲', '乙')),
FUN = sum)
## type new_type x
## 1 A FALSE 66
## 2 B TRUE 78
## 3 D TRUE 156
2.8.2.aggregate.formula 🔗
当 aggregate 作为 aggregate.formula 来使用时,作用与 aggregate.data.frame 相似,但有以下几处区别。
区别之一如下,aggregate.formula 调用函数的列名称仍与输入时一致,而 aggregate.data.frame 调用函数的列名称变成了 x。
aggregate(value ~ type + new_type, data = my_dataframe, FUN = sum)
## type new_type value
## 1 A 丁 66
## 2 D 甲 84
## 3 B 乙 78
## 4 D 乙 72
区别之二如下,aggregate.formula 用单独的 subset 参数来设置组别过滤,输出结果会仅留下过滤后的组别,而 aggregate.data.frame 仍然会保留原始组别。
aggregate(
value ~ type + new_type,
data = my_dataframe,
FUN = sum,
subset = my_dataframe[, "new_type"] %in% c('甲', '乙')
)
## type new_type value
## 1 D 甲 84
## 2 B 乙 78
## 3 D 乙 72
区别之三如下,当需要对多个字段进行汇总统计时,aggregate.formula 要在公式左边用cbind(value, new_value)
的方式来写,如果公式写成value + new_value ~ type + new_type
,会被当做对 value 与 new_value 之和进行汇总。
my_dataframe$new_value <- sample(1:100, 24)
aggregate(cbind(value, new_value) ~ type + new_type,
data = my_dataframe,
FUN = sum)
## type new_type value new_value
## 1 A 丁 66 290
## 2 D 甲 84 228
## 3 B 乙 78 246
## 4 D 乙 72 455
# aggregate(
# x = my_dataframe[, c('value', 'new_value')],
# by = list(type = my_dataframe$type, new_type = my_dataframe$new_type),
# FUN = sum)
三、一些关于数据处理的思考题 🔗
3.1.第一个思考题 🔗
题目:已知有 data1 和 data2,想要得到 data3 的结果。
data1 <- data.frame(
id = c('A', 'B', 'C'),
code = c('code1', 'code2,code3', 'code4,code5,code6'))
print(data1)
## id code
## 1 A code1
## 2 B code2,code3
## 3 C code4,code5,code6
data2 <- data.frame(code = paste0('code', c(1:6)),
fee = c(100, 1000, 10000, 500, 5000, 1314))
print(data2)
## code fee
## 1 code1 100
## 2 code2 1000
## 3 code3 10000
## 4 code4 500
## 5 code5 5000
## 6 code6 1314
data3 <- data.frame(
id = c('A', 'B', 'C'),
code = c('code1', 'code2,code3', 'code4,code5,code6'),
fee = c(100, 11000, 6814))
print(data3)
## id code fee
## 1 A code1 100
## 2 B code2,code3 11000
## 3 C code4,code5,code6 6814
这道思考题的解题重点在于要先把 data1 中的 code 列拆开,然后匹配每一个单独的 code 对应的 fee,最后汇总。
- 第一种解法,在 R 中。
自定义字符串拆分函数,然后用 apply 族调用函数创建中间表 new_data,新表第一列按 data1 中 code 列每行拆分后的长度重复生成新 id 列,对应拆分后的内容放入新 code 列中。最后按 code 列关联,按 id 列汇总。
library(data.table)
setDT(data1)
setDT(data2)
# 定义一个函数,将字符串按照逗号拆分
split_codes <- function(x) {
strsplit(x, ",")[[1]]
}
# 将函数应用到整个 data1 表中的 code 列
new_data <-
data.table(id = rep(data1$id, sapply(data1$code, function(x)
length(split_codes(x)))),
code = unlist(lapply(data1$code, split_codes)))
# 关联 fee,汇总结果
data3 <-
data2[new_data, on = .(code)][, by = .(id), .(sum_fee = sum(fee))][data1, on = .(id)]
拆分字符串的步骤也可以直接放到 data.table 表格中进行。
# 定义一个函数,将字符串按照逗号拆分
split_codes <- function(x) {
tstrsplit(x, ",")
}
# 将函数应用到整个data1表中的code列
new_data <-
data1[, lapply(.SD, split_codes), by = id][, .(code = unlist(code)), by = id]
- 第二种解法,在 ORACLE 中。
ORACLE 本身是存储数据的数据库,要是刚好某张表被设计成如 data1 那样,那就不得不先递归得到具体每个 code。讲真,ORACLE 里面条条框框限制太多,做一些较为复杂的数据处理真得有些笨重。
create table oracle_table (id VARCHAR2(4), code varchar2(400));
insert all
into oracle_table (id, code)
values ('A','code1')
into oracle_table (id, code)
values ('B', 'code2,code3')
into oracle_table (id, code)
values ('C', 'code4,code5,code6')
select * from dual;
--1 需要递归的方法得到字符串拆分的结果
SELECT TRIM(REGEXP_SUBSTR('code1,code2,code3', '[^,]+', 1, LEVEL)) AS code
FROM dual
CONNECT BY REGEXP_SUBSTR('code1,code2,code3', '[^,]+', 1, LEVEL) IS NOT NULL;
--2 将列拆分到行
select t1.id, regexp_substr(t1.code, '[^,]+', 1, level) pcode
from oracle_table t1
CONNECT BY LEVEL <= REGEXP_COUNT(t1.code, ',') + 1
AND PRIOR id = id
AND PRIOR SYS_GUID() IS NOT NULL
ORDER BY t1.id;
3.2.第二个思考题 🔗
题目:已知有一个目标文本text <- 'a,b,c'
,判断数据框 my_dataframe2 中的 new_name 列是否与目标文本有交集。
# 目标文本
text <- 'a,b,c'
# 待匹配数据
set.seed(131415926)
letters <- c("a", "b", "c", "d", "e", "f", "g")
my_dataframe2 <- data.frame(
id = c(1:100),
name1 = sample(letters, 100, replace = TRUE),
name2 = sample(letters, 100, replace = TRUE)
)
my_dataframe2$new_name <-
mapply(
FUN = function(x, y) {
paste(x, y, sep = ",")
},
my_dataframe2$name1,
my_dataframe2$name2
)
head(my_dataframe2[, c("id", "new_name")], n = 4)
## id new_name
## 1 1 d,c
## 2 2 b,a
## 3 3 e,d
## 4 4 f,f
- 第一种解法,将整体比较的过程封装成一个函数。这样做的好处是,换成比较别的目标文本和别的列时,可以调用同一个函数。
# 自定义函数
rule_intersection <- function(text, my_dataframe) {
if (is.na(text) == FALSE) {
set1 <- strsplit(text, ",")[[1]]
set2_list <- strsplit(my_dataframe[, "new_name"], ",")
my_dataframe$result <- sapply(
X = set2_list,
FUN = function(set2) {
if (length(intersect(set1, set2)) > 0) {
return(0)
} else {
return(1)
}
}
)
} else {
my_dataframe$result <- rep(1, nrow(my_dataframe))
}
return(my_dataframe)
}
# 对目标文本和数据框使用函数,查看结果
head(rule_intersection(text, my_dataframe2[, c("id", "new_name")]), 4)
## id new_name result
## 1 1 d,c 0
## 2 2 b,a 0
## 3 3 e,d 1
## 4 4 f,f 1
- 第二种解法,应用 sapply 批量处理单次比较。
# 分别对目标文本和 new_name 列做拆分
target_vector <- strsplit(text, split = ",")[[1]]
new_name_vectors <- strsplit(my_dataframe2$new_name, split = ",")
# 应用 sapply 批量比较是否有交集
my_dataframe2$result2 <-
sapply(new_name_vectors, function(x) any(x %in% target_vector))
# 查看结果
head(my_dataframe2[, c("id", "new_name","result2")], 4)
## id new_name result2
## 1 1 d,c TRUE
## 2 2 b,a TRUE
## 3 3 e,d FALSE
## 4 4 f,f FALSE
这里用 microbenchmark 包来比较两种解法的执行效率。在打印的结果中,数值的单位是毫秒,其中 neval 代表代码片段的执行次数,而 min、lq、mean、median、uq、max分别代表代表多次执行用时的最小值、25%分位数、均值、中位数、75%分位数、最大值。
根据比较结果,第二种解法执行效率更快。
library(microbenchmark)
# 比较两个代码片段的执行效率
results <- microbenchmark(
# 第一种解法
code_chunk_1 = {
rule_intersection(text, my_dataframe2[, c("id", "new_name")])
},
# 第二种解法
code_chunk_2 = {
target_vector <- strsplit(text, split = ",")[[1]]
new_name_vectors <- strsplit(my_dataframe2$new_name, split = ",")
my_dataframe2$result3 <-
sapply(new_name_vectors, function(x)
any(x %in% target_vector))
},
# 指定每个代码片段执行的次数
times = 100)
# 打印结果
print(results)
## Unit: microseconds
## expr min lq mean median uq max neval
## code_chunk_1 1031.1 1056.35 1202.337 1067.45 1088.0 8504.9 100
## code_chunk_2 251.7 261.00 287.825 266.40 273.2 1391.0 100
四、总结 🔗
- apply -> lapply/sapply:只有 apply 是根据指定维度来调用函数,其他 apply 族函数都是对所有元素来调用函数。
- lapply -> sapply:简化输出结果,如将列表简化为向量。
- lapply -> vapply:对输出结果做类型控制。
- lapply -> eapply:调用函数的范围扩大到环境中的所有数据对象。
- lapply -> rapply:针对嵌套列表的情况,调用函数的范围递归至每一层每个元素。
- sapply -> mapply:可输入多个数据对象,但调用函数的参数数量需要跟数据对象的数量互相适配。
- sapply -> tapply:可以分组调用函数。
- tapply -> aggregate:指定分组依据的方式更灵活,可以筛选子集。
PS. 在今天下班骑自行车回去的路上,有一辆汉警快骑(摩托车)快速略过我身旁,联想到去年参加警营开放日时汉警快骑们又帅又酷的表演,俺在心里说“啊,要追上去”,然而尽管使出全力瞪踏板,也还是眼见着快骑没于车流中,叹息一秒后很快想通,毕竟自行车是追不上摩托车的嘛。
-
根据 AI 的答案,在计算机中,浮点数是一种用于表示实数的数据类型。单精度浮点数和双精度浮点数都是浮点数的一种,它们的区别在于所占用的内存空间和精度不同。单精度浮点数通常占用4个字节(32位)的内存空间,可以表示的数值范围为-3.40E+38~3.40E+38,有效数字位数为7位。而双精度浮点数通常占用8个字节(64位)的内存空间,可以表示的数值范围为-1.79E+308~1.79E+308,有效数字位数为15位。因此,双精度浮点数比单精度浮点数具有更高的精度和更大的数值范围,但同时也需要更多的内存空间。 ↩︎
-
Date
:用于存储日期数据,通常用整数表示,数值为从1970年1月1日经过的天数。POSIXct
:用于存储日期时间数据,可以仅包含日期部分,也可以同时包含日期和时间。它将日期时间保存为从1970年1月1日零时到该日期时间的时间间隔秒数。POSIXlt
:也用于存储日期时间数据,可以仅包含日期部分,也可以同时包含日期和时间。它将日期时间保存为一个包含年、月、日、星期、时、分、秒等成分的列表。 ↩︎