本文内容:
- 基础函数操作数据框
- sqldf包使用SQL查询数据框
- data.table包操作数据
- dplyr管道操作处理数据
- rlist包处理嵌套数据结构
使用内置函数操作数据框
数据框的本质是一个由向量构成的列表,由于列长度相同,所以可以当做矩阵进行访问和操作。比如选择满足特定条件的行,使用[]
符号,第一个参数提供一个逻辑向量,第二个参数留空。
本文大部分的代码都是基于一组产品的虚拟数据。我们先将数据载入,然后学习怎么用不同的方法操作数据。
代码语言:javascript复制if(!require(readr)) install.packages("readr")
#> 载入需要的程辑包:readr
product_info = read_csv("../../R/dataset/product-info.csv")
#> Parsed with column specification:
#> cols(
#> id = col_character(),
#> name = col_character(),
#> type = col_character(),
#> class = col_character(),
#> released = col_character()
#> )
product_info
#> # A tibble: 6 x 5
#> id name type class released
#> <chr> <chr> <chr> <chr> <chr>
#> 1 T01 SupCar toy vehicle yes
#> 2 T02 SupPlane toy vehicle no
#> 3 M01 JeepX model vehicle yes
#> 4 M02 AircraftX model vehicle yes
#> 5 M03 Runner model people yes
#> 6 M04 Dancer model people no
当数据以数据框的形式载入内存后,我们可以使用下面的代码查看每一列的类型:
代码语言:javascript复制sapply(product_info, class)
#> id name type class released
#> "character" "character" "character" "character" "character"
注意read_csv
函数载入的数据框与内置函数read.csv
函数是不同的,主要体现在不会将字符串转换为因子变量,当然前者的速度要快得多。
接下来我们正式学习用R内置的函数操作数据框进行分析和统计的一些方法。
内置函数操作数据框
选取type
为toy
的行:
product_info[product_info$type == "toy", ]
#> # A tibble: 2 x 5
#> id name type class released
#> <chr> <chr> <chr> <chr> <chr>
#> 1 T01 SupCar toy vehicle yes
#> 2 T02 SupPlane toy vehicle no
或选取released
为no
的行:
product_info[product_info$released == "no", ]
#> # A tibble: 2 x 5
#> id name type class released
#> <chr> <chr> <chr> <chr> <chr>
#> 1 T02 SupPlane toy vehicle no
#> 2 M04 Dancer model people no
对列进行筛选需要将第1个参数留空,给第2个参数提供字符向量。
代码语言:javascript复制product_info[, c("id", "name", "type")]
#> # A tibble: 6 x 3
#> id name type
#> <chr> <chr> <chr>
#> 1 T01 SupCar toy
#> 2 T02 SupPlane toy
#> 3 M01 JeepX model
#> 4 M02 AircraftX model
#> 5 M03 Runner model
#> 6 M04 Dancer model
行列筛选也是可以的,我们只要组合前面的两种情况即可。
代码语言:javascript复制product_info[product_info$type == "toy", c("name", "class", "released")]
#> # A tibble: 2 x 3
#> name class released
#> <chr> <chr> <chr>
#> 1 SupCar vehicle yes
#> 2 SupPlane vehicle no
内置函数subset()
可以简化取子集操作的过程:
subset(product_info,
subset = type == "model" & released == "yes",
select = name:class)
#> # A tibble: 3 x 3
#> name type class
#> <chr> <chr> <chr>
#> 1 JeepX model vehicle
#> 2 AircraftX model vehicle
#> 3 Runner model people
使用with()
函数在数据框的语义中计算表达式,即可以直接使用数据框的列名,而不必重复指定数据框:
with(product_info, name[released == "no"])
#> [1] "SupPlane" "Dancer"
除了构建子集,表达式还可以用来统计每列各个可能值出现的频数。
代码语言:javascript复制with(product_info, table(type[released == "yes"]))
#>
#> model toy
#> 3 1
除了产品信息表,还有一张产品属性的统计表:
代码语言:javascript复制product_stats = read_csv("../../R/dataset/product-stats.csv")
#> Parsed with column specification:
#> cols(
#> id = col_character(),
#> material = col_character(),
#> size = col_integer(),
#> weight = col_double()
#> )
product_stats
#> # A tibble: 6 x 4
#> id material size weight
#> <chr> <chr> <int> <dbl>
#> 1 T01 Metal 120 10
#> 2 T02 Metal 350 45
#> 3 M01 Plastics 50 NA
#> 4 M02 Plastics 85 3
#> 5 M03 Wood 15 NA
#> 6 M04 Wood 16 0.6
如果现在要获取尺寸最大的前3个产品的名字该怎么办?
一种方法是将product_stats
按尺寸降序排列,选择前3个记录的id,然后用id值筛选product_info
的行:
top3_id = unlist(product_stats[order(product_stats$size, decreasing = TRUE), "id"])[1:3]
product_info[product_info$id %in% top3_id, ]
#> # A tibble: 3 x 5
#> id name type class released
#> <chr> <chr> <chr> <chr> <chr>
#> 1 T01 SupCar toy vehicle yes
#> 2 T02 SupPlane toy vehicle no
#> 3 M02 AircraftX model vehicle yes
我们用比较冗长的方式完成了任务。但仔细在想想,两个数据框是通过id
连接到一起的,我们可以把它们合并到一起,然后执行提取操作:
product_table = merge(product_info, product_stats, by = "id")
product_table
#> id name type class released material size weight
#> 1 M01 JeepX model vehicle yes Plastics 50 NA
#> 2 M02 AircraftX model vehicle yes Plastics 85 3.0
#> 3 M03 Runner model people yes Wood 15 NA
#> 4 M04 Dancer model people no Wood 16 0.6
#> 5 T01 SupCar toy vehicle yes Metal 120 10.0
#> 6 T02 SupPlane toy vehicle no Metal 350 45.0
现在通过合并的数据框,我们可以根据任意一列排序数据框,而不需要处理其他的表格数据:
代码语言:javascript复制product_table[order(product_table$size), ]
#> id name type class released material size weight
#> 3 M03 Runner model people yes Wood 15 NA
#> 4 M04 Dancer model people no Wood 16 0.6
#> 1 M01 JeepX model vehicle yes Plastics 50 NA
#> 2 M02 AircraftX model vehicle yes Plastics 85 3.0
#> 5 T01 SupCar toy vehicle yes Metal 120 10.0
#> 6 T02 SupPlane toy vehicle no Metal 350 45.0
前面的问题我们也可以利用合并的数据框加以解决:
代码语言:javascript复制product_table[order(product_table$size, decreasing = TRUE), "name"][1:3]
#> [1] "SupPlane" "SupCar" "AircraftX"
有时候我们需要生成新数据框来对原始数据基础上进行调整和处理,从而避免破坏原始数据。transform()
函数可以帮助我们完成这类任务,例如:
transform(product_table,
released = ifelse(released == "yes", TRUE, FALSE),
density = weight / size)
#> id name type class released material size weight density
#> 1 M01 JeepX model vehicle TRUE Plastics 50 NA NA
#> 2 M02 AircraftX model vehicle TRUE Plastics 85 3.0 0.0353
#> 3 M03 Runner model people TRUE Wood 15 NA NA
#> 4 M04 Dancer model people FALSE Wood 16 0.6 0.0375
#> 5 T01 SupCar toy vehicle TRUE Metal 120 10.0 0.0833
#> 6 T02 SupPlane toy vehicle FALSE Metal 350 45.0 0.1286
前面数据中我们看到有一些缺失值(用NA
表示),很多时候我们不希望数据出现任何缺失值,因此需要某种办法处理它们。为了演示处理的方法,我们再载入一张包含缺失值的表,包含每件产品的质量、耐久性、防水性的测试结果。
product_tests = read_csv("../../R/dataset/product-tests.csv")
#> Parsed with column specification:
#> cols(
#> id = col_character(),
#> quality = col_integer(),
#> durability = col_integer(),
#> waterproof = col_character()
#> )
product_tests
#> # A tibble: 6 x 4
#> id quality durability waterproof
#> <chr> <int> <int> <chr>
#> 1 T01 NA 10 no
#> 2 T02 10 9 no
#> 3 M01 6 4 yes
#> 4 M02 6 5 yes
#> 5 M03 5 NA yes
#> 6 M04 6 6 yes
na.omit()
可以删除所有包含缺失值的行:
na.omit(product_tests)
#> # A tibble: 4 x 4
#> id quality durability waterproof
#> <chr> <int> <int> <chr>
#> 1 T02 10 9 no
#> 2 M01 6 4 yes
#> 3 M02 6 5 yes
#> 4 M04 6 6 yes
另外,函数complete.cases()
可以返回逻辑向量,表明某行是否完整。
complete.cases(product_tests)
#> [1] FALSE TRUE TRUE TRUE FALSE TRUE
利用该函数可以筛选数据框,比如获得不含缺失值的id
值:
product_tests[complete.cases(product_tests), "id"]
#> # A tibble: 4 x 1
#> id
#> <chr>
#> 1 T02
#> 2 M01
#> 3 M02
#> 4 M04
前面给出的3个表格有共同的id
列,可惜R里面内置函数只能一次合并2个数据框:
product_full = merge(product_table, product_tests, by = "id")
product_full
#> id name type class released material size weight quality
#> 1 M01 JeepX model vehicle yes Plastics 50 NA 6
#> 2 M02 AircraftX model vehicle yes Plastics 85 3.0 6
#> 3 M03 Runner model people yes Wood 15 NA 5
#> 4 M04 Dancer model people no Wood 16 0.6 6
#> 5 T01 SupCar toy vehicle yes Metal 120 10.0 NA
#> 6 T02 SupPlane toy vehicle no Metal 350 45.0 10
#> durability waterproof
#> 1 4 yes
#> 2 5 yes
#> 3 NA yes
#> 4 6 yes
#> 5 10 no
#> 6 9 no
对完全合并好的表格,我们利用tapply()
函数(apply
家族成员)可以进行统计,该函数专门用于处理表格数据,使用某些方法根据某列队另一列的数据进行统计。
例如根据type列计算quality列的均值:
代码语言:javascript复制mean_quality1 = tapply(product_full$quality,
list(product_full$type),
mean, na.rm=TRUE)
mean_quality1
#> model toy
#> 5.75 10.00
返回的结果看起来是个数值向量,我们使用str()
看看:
str(mean_quality1)
#> num [1:2(1d)] 5.75 10
#> - attr(*, "dimnames")=List of 1
#> ..$ : chr [1:2] "model" "toy"
实际上,这是个一维数组:
代码语言:javascript复制is.array(mean_quality1)
#> [1] TRUE
tapply()
返回的是一个数组,而不是简单的数值向量,因此可以方便地计算多组操作。
例如计算每一对type
和class
组合的quality
均值:
mean_quality2 = tapply(product_full$quality,
list(product_full$type, product_full$class),
mean, na.rm = TRUE)
mean_quality2
#> people vehicle
#> model 5.5 6
#> toy NA 10
对于二维数组,我们可以使用两个参数来获取其中的值:
代码语言:javascript复制typeof(mean_quality2)
#> [1] "double"
class(mean_quality2)
#> [1] "matrix"
mean_quality2["model", "vehicle"]
#> [1] 6
同理我们可以根据多列分组,使用with()
可以避免反复输入product_full
:
mean_quality3 = with(product_full,
tapply(quality, list(type, material, released),
mean, na.rm = TRUE))
mean_quality3
#> , , no
#>
#> Metal Plastics Wood
#> model NA NA 6
#> toy 10 NA NA
#>
#> , , yes
#>
#> Metal Plastics Wood
#> model NA 6 5
#> toy NaN NA NA
使用3个参数可以获取单元格中的值:
代码语言:javascript复制mean_quality3["model", "Wood", "yes"]
#> [1] 5
reshape2重塑数据框
前面我们学习了如何筛选、排序、合并和汇总数据框,有时候我们需要做些更复杂的操作。
例如下面数据包含两种产品不同日期的质量和耐久性的测试结果:
代码语言:javascript复制toy_tests = read_csv("../../R/dataset/product-toy-tests.csv")
#> Parsed with column specification:
#> cols(
#> id = col_character(),
#> date = col_integer(),
#> sample = col_integer(),
#> quality = col_integer(),
#> durability = col_integer()
#> )
toy_tests
#> # A tibble: 8 x 5
#> id date sample quality durability
#> <chr> <int> <int> <int> <int>
#> 1 T01 20160201 100 9 9
#> 2 T01 20160302 150 10 9
#> 3 T01 20160405 180 9 10
#> 4 T01 20160502 140 9 9
#> 5 T02 20160201 70 7 9
#> 6 T02 20160303 75 8 8
#> 7 T02 20160403 90 9 8
#> 8 T02 20160502 85 10 9
如果需要同时比较两种产品的质量和耐久性,这种格式就比较麻烦,如果是下面的格式就好了:
代码语言:javascript复制date T01 T02
20160201 9 9
2016 10 9
reshape2
包就是用来搞定这种任务的,如果没有安装,运行下面代码:
install.packages("reshape2")
安装成功后,我们就可以使用dcast()
来转换数据,用于比较:
library(reshape2)
toy_quality = dcast(toy_tests, date ~ id, value.var = "quality")
toy_quality
#> date T01 T02
#> 1 20160201 9 7
#> 2 20160302 10 NA
#> 3 20160303 NA 8
#> 4 20160403 NA 9
#> 5 20160405 9 NA
#> 6 20160502 9 10
上述代码重塑了toy_tests
让date
列被共享,id
值被单独分割为列,每个date
与id
对应的值是quality
。
可以看到数据中存在缺失值,有一种叫末次观测值结转法(LOCF)可以填补缺失值,当非缺失值后面紧跟一个缺失值时,就用该缺失值填补后面的缺失值,直到所有缺失值都被填满。zoo
包提供了LOCF的一个实现,使用下面代码安装:
install.packages("zoo")
下面用一组简单的向量演示:
代码语言:javascript复制library(zoo)
#>
#> 载入程辑包:'zoo'
#> The following objects are masked from 'package:base':
#>
#> as.Date, as.Date.numeric
na.locf(c(1, 2, NA, NA, 3, 1, NA, 2, NA))
#> [1] 1 2 2 2 3 1 1 2 2
同样的方法我们可以应用于现在处理的数据:
代码语言:javascript复制na.locf(toy_quality$T01)
#> [1] 9 10 10 10 9 9
如果需要填补的数据很多,包含上千个产品,更好的做法是使用lapply
进行自动分配:
toy_quality[-1] = lapply(toy_quality[-1], na.locf )
toy_quality
#> date T01 T02
#> 1 20160201 9 7
#> 2 20160302 10 7
#> 3 20160303 10 8
#> 4 20160403 10 9
#> 5 20160405 9 9
#> 6 20160502 9 10