「R」数据操作(一)

2020-07-02 15:51:51 浏览数 (1)

本文内容:

  • 基础函数操作数据框
  • 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内置的函数操作数据框进行分析和统计的一些方法。

内置函数操作数据框

选取typetoy的行:

代码语言:javascript复制
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

或选取releasedno的行:

代码语言:javascript复制
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()可以简化取子集操作的过程:

代码语言:javascript复制
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()函数在数据框的语义中计算表达式,即可以直接使用数据框的列名,而不必重复指定数据框:

代码语言:javascript复制
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的行:

代码语言:javascript复制
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连接到一起的,我们可以把它们合并到一起,然后执行提取操作:

代码语言:javascript复制
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()函数可以帮助我们完成这类任务,例如:

代码语言:javascript复制
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表示),很多时候我们不希望数据出现任何缺失值,因此需要某种办法处理它们。为了演示处理的方法,我们再载入一张包含缺失值的表,包含每件产品的质量、耐久性、防水性的测试结果。

代码语言:javascript复制
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()可以删除所有包含缺失值的行:

代码语言:javascript复制
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()可以返回逻辑向量,表明某行是否完整。

代码语言:javascript复制
complete.cases(product_tests)
#> [1] FALSE  TRUE  TRUE  TRUE FALSE  TRUE

利用该函数可以筛选数据框,比如获得不含缺失值的id值:

代码语言:javascript复制
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个数据框:

代码语言:javascript复制
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()看看:

代码语言:javascript复制
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()返回的是一个数组,而不是简单的数值向量,因此可以方便地计算多组操作。

例如计算每一对typeclass组合的quality均值:

代码语言:javascript复制
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

代码语言:javascript复制
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包就是用来搞定这种任务的,如果没有安装,运行下面代码:

代码语言:javascript复制
install.packages("reshape2")

安装成功后,我们就可以使用dcast()来转换数据,用于比较:

代码语言:javascript复制
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_testsdate列被共享,id值被单独分割为列,每个dateid对应的值是quality

可以看到数据中存在缺失值,有一种叫末次观测值结转法(LOCF)可以填补缺失值,当非缺失值后面紧跟一个缺失值时,就用该缺失值填补后面的缺失值,直到所有缺失值都被填满。zoo包提供了LOCF的一个实现,使用下面代码安装:

代码语言:javascript复制
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进行自动分配:

代码语言:javascript复制
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

0 人点赞