R 语言入门 —— tidyverse

R 语言入门 —— tidyverse

tidyverseR 语言中专为数据科学而设计的集成包,其内所有的包具有相同的设计理念、语法以及数据结构。大名鼎鼎的画图包 ggplot2 便是其中之一,其管道操作让代码看起来更加简洁。既然提到了管道操作,那就让我们先来介绍一下什么是管道操作,在这之前先把包给装了

我们只需一键安装 tidyverse,就可以使用其下的所有包

# 从 CRAN 库中安装
install.packages("tidyverse")

# 安装开发者版本
# install.packages("devtools")
devtools::install_github("tidyverse/tidyverse")

导入 tidyverse

library(tidyverse)
# ── Attaching packages ───────────────────────────────────────── tidyverse 1.3.1 ──
# ✓ ggplot2 3.3.5     ✓ purrr   0.3.4
# ✓ tibble  3.1.6     ✓ dplyr   1.0.7
# ✓ tidyr   1.1.4     ✓ stringr 1.4.0
# ✓ readr   2.1.0     ✓ forcats 0.5.1
# ── Conflicts ──────────────────────────────────────────── tidyverse_conflicts() ──
# x dplyr::filter() masks stats::filter()
# x dplyr::lag()    masks stats::lag()

默认会将上面打钩的包一并导入,其他包则需要手动导入,Conflicts 下面的函数表示存在冲突,需要使用包名加函数名的方式访问

1. 管道操作

管道操作,顾名思义就是让数据看起来像流水一样从管道一头流向另一头,或者说链式调用,数据没通过一层函数就进行了一次加工,如流水线一般,类似于 Linux 里面的 | 符号。该功能由 magrittr 提供

安装完 tidyverse 之后,便可以导入了,如果要单独安装该包也非常简单

install.packages("magrittr")
# 或
devtools::install_github("tidyverse/magrittr")

导入包

library(magrittr)  # version 2.0.1

magrittr 主要提供了 4 个管道操作符:

  • %>%:向右操作符,即将数据传递到右侧的函数中
  • %T>%Tee 操作符,相当于数据在这里拐了个弯就往下走了,不会修改数据
  • %$%:解释操作符
  • %<>%:复合赋值管道操作符,将走完流程的数据返回赋值给开头的赋值变量

%>% 操作符

将左边的结果传递给右边的函数作为第一个参数值,其结果可以继续向右边传递,默认会将 %>% 符号左边计算出的值作为右边函数的的第一个参数,例如下面的两种方式是等价的

管道操作 嵌套函数
x %>% f f(x)
x %>% f(y) f(x, y)
x %>% f %>% g %>% h h(g(f(x)))
x <- 3
f <- function(x, y=1) {
    return(x+y) }
x %>% f
# [1] 4
x %>% f(2)
# [1] 5

使用 . 符号作为占位符,表示管道左边传过来的数据变量,下面的代码是等价的

管道符 函数
x %>% f(y, .) f(y, x)
x %>% f(y, z = .) f(y, z = x)
x %>% f(y = .,x = 5)
# [1] 8
x %>% f(y = 3,x = .)
# [1] 6

在右侧表达式中可以多次使用占位符。

x <- matrix(1:12, nrow = 3, ncol = 4)
f <- function(x, y, z) {
    return(x * y * z) }
x %>% f(y = nrow(.), z = ncol(.))
#      [,1] [,2] [,3] [,4]
# [1,]   12   48   84  120
# [2,]   24   60   96  132
# [3,]   36   72  108  144

但是,当占位符出现在嵌套表达式中时,第一个参数不能省略

x %>% {
   f(y = nrow(.), z = ncol(.))}
# Error in f(y = nrow(.), z = ncol(.)) : 缺少参数"x",也没有缺省值
x %>% {
   f(1, y = nrow(.), z = ncol(.))}
# [1] 12

任何以 . 开头的管道序列将会返回一个一元函数

f <- . %>% cos %>% sin 
f(10)
# [1] -0.7440231
# 等价于
h <- function(.) sin(cos(.)) 
h(10)
# [1] -0.7440231

传递到代码块

rnorm(10) %>% multiply_by(5) %>% add(5) %>%
{
   
 print("Mean:", mean(.))
 sort(.) %>% head(5)
}
# [1] "Mean:"
# [1] -0.7076385 -0.3230133  0.9298034  0.9390084  2.5007525

传递到函数

rnorm(10) %>% add(1) %>% `*`(10) %>% (
  function(x) {
   
    if (x[1] > 5) {
   
      x-5
    } else x
  }
)
# [1] -5.056051  5.214681 16.122495 22.311603 22.431362 15.828250  1.785964 -2.969273 26.003416
# [10] 12.304749

%T>% 操作符

其与 %>% 的区别是其结果不能向右传递,而是将 %T>% 左边的结果继续向右传递下去,%T>% 通常用来输出图形、打印结果到屏幕或者输出到文件,然后继续后面的 %>% 操作。其左边的值相当于做了两次传递,分别传递给 %T>% 右侧表达式以及其后面的 %>% 管道符的右侧表达式,也可以理解为在一串管道序列中插入了一个操作,但并不会影响其后续的管道序列

a <- 5
a %>% cos %T>% print %>% sin
# [1] 0.2836622
# [1] 0.2798734

%$% 操作符

通常 %$% 的左边是数据框,而右边的函数可直接使用该数据框中的变量

x <- matrix(1:12, nrow = 3, ncol = 4)
data.frame(x = x[,1], y = x[,2], z = x[,3]) %$% .[which(x > 5),]
# [1] x y z
# <0 行> (或0-长度的row.names)
data.frame(x = x[,1], y = x[,2], z = x[,3]) %$% .[which(y > 5),]
#   x y z
# 3 3 6 9

相当于

df <- data.frame(x = x[,1], y = x[,2], z = x[,3])
df[which(df$y>5),]
#   x y z
# 3 3 6 9

%<>% 操作符

只能出现在所有管道符的第一个,用于在一长串处理管道操作之后直接将最终的结果赋值到最左边的对象上

a <- 1:10
print(a)
#  [1]  1  2  3  4  5  6  7  8  9 10
a %<>% exp %>% sqrt
print(a)
# [1]   1.648721   2.718282   4.481689   7.389056  12.182494  20.085537  33.115452  54.598150  90.017131
# [10] 148.413159

a 的值变为了 sqrt(exp(a))

通用函数操作符

除了上面 4 个主要的管道符之外,还将一些常用的操作符函数起了一个见名知意的别名,如下表

函数 操作符 函数 操作符
extract `[`(索引取值) inset `[<-`(索引赋值)
extract2 `[[` inset2 `[[<-`
add `+` subtract `-`
multiply_by `*` divide_by `/`
use_series `$`(属性访问) raise_to_power `^`
multiply_by_matrix `%*%` divide_by_int `%/%`
mod `%%` is_in `%in%`
and `&` or `` `
equals `==` not `!`
is_greater_than `>` is_less_than `<`
is_weakly_greater_than `>=` is_weakly_less_than `<=`
set_colnames `colnames<-` set_rownames `rownames<-`
set_names `names<-` set_class `class<-`
set_attributes `attributes<-` set_attr `attr<-`

在上面这张表中,除了我们之前提到的运算符函数,还看到了一些特殊的操作符, `[` 表示的是方括号索引取值,而 `[<-` 则是其对应的赋值版本,`[[` 对应的是双方括号取值,`$` 对应的是属性访问。

a <- list(x = 3, y = 4, r = 10)
a$x
# [1] 3
`$`(a, "x")      # 用函数获取属性值
# [1] 3
`$`(a, "y")      
# [1] 4
`[[`(a, 3)       # 用函数获取第三个值
# [1] 10
`[[<-`(a, 3, 8)  # 设置值第三个值
# $x
# [1] 3
# 
# $y
# [1] 4
# 
# $r
# [1] 8
names(a)          # 获取名称
# [1] "x" "y" "r"
`names<-`(a, c("p", "q", "r"))  # 设置名称
#  p  q  r 
#  3  4 10 
names(a) <- c("p", "q", "r")
a
#  p  q  r 
#  3  4 10 

我们可以看到,使用操作符函数的设置方式返回的是一个新的对象,而使用操作符是会原地修改对象,正因为如此使用上述函数别名才可以让流程继续执行下去,而不会提前终止

seq(10) %>% `*`(5) %>% `+`(5)
# [1] 10 15 20 25 30 35 40 45 50 55
seq(10) %>% multiply_by(5) %>% add(5)
# [1] 10 15 20 25 30 35 40 45 50 55

2. 数据结构

R 语言产生到现在也挺久了,一些 10 年或 20 年前很有用的东西现在不一定同样有用。在不破坏现有代码的情况下想要改变 R 的基础数据结构是很困难的,所以大多数的创新都是在开发新包中。

在这里,我们将要介绍 tidyverse 包最基础、最通用数据结构:tibble,该数据结构是 tidyverse 的特性之一,几乎所有的数据处理函数都是面向 tibble 类型的对象。

其结构类似于 R 传统的 data.frame 结构,一般能应用于 data.frame 的函数都可以对 tibble 结构使用,反之亦可,两种数据结构可以相互转换。

首先,我们导入该包

library(tidyverse)
# 或者
library(tibble)     # version 3.1.6

构造 tibble

绝大多数的 R 包都是针对常规的 data.frame对象进行操作,如果要将其转换为 tibble,可以使用 as_tibble() 函数

as_tibble(iris)
# # A tibble: 150 × 5
#    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#           <dbl>       <dbl>        <dbl>       <dbl> <fct>  
#  1          5.1         3.5          1.4         0.2 setosa 
#  2          4.9         3            1.4         0.2 setosa 
#  3          4.7         3.2          1.3         0.2 setosa 
#  4          4.6         3.1          1.5         0.2 setosa 
#  5          5           3.6          1.4         0.2 setosa 
#  6          5.4         3.9          1.7         0.4 setosa 
#  7          4.6         3.4          1.4         0.3 setosa 
#  8          5           3.4          1.5         0.2 setosa 
#  9          4.4         2.9          1.4         0.2 setosa 
# 10          4.9         3.1          1.5         0.1 setosa 
# … with 140 more rows
# # … with 140 more rows

可以看到,对象的值并不会全部打印出,默认显示前面 10 行数据,看起来比 data.frame 全屏输出好多了,还能看出每列数据的类型。

使用 tibble 函数可以从单个向量创建一个新的 tibble,该函数会自动循环长度为 1 的输入,将其扩展到与其他数据长度一致,并允许您引用刚刚创建的变量,如下所示

tibble(
  a = 3:5, 
  b = 2, 
  c = 2 * a + b
)
# # A tibble: 3 × 3
#       a     b     c
#   <int> <dbl> <dbl>
# 1     3     2     8
# 2     4     2    10
# 3     5     2    12

tibble 不会更改输入数据的类型(比如,不会将字符串转换为因子),也不会更改变量的名称或者创建行名。

tibble 允许使用无效的 R 变量名称作为列名,也就是非语法名称。例如,当某些变量不以字母开头,或者名称中包含特殊的字符(如空格等),而为了引用这些变量,需要用反引号 ` 将它们包裹起来

tb <- tibble(
  `+` = "plus",
  `2000` = "integer",
  `:)` = "smile", 
)
tb
# # A tibble: 1 × 3
#   `+`   `2000`  `:)` 
#   <chr> <chr>   <chr>
# 1 plus  integer smile
tb$`+`
# [1] "plus"
tb$`:)`
# [1] "smile"

引用非正规变量名都需要用反引号包裹。使用 tribble 函数可以创建 tibble 对象,该函数主要用于创建小数据,且具有较好的可读性,列名使用公式来定义(以 ~ 开头),条目之间用逗号分隔,所见即所得,通常还可以添加一行 # 开头的注释,来标明表头与数据的位置。

tribble(
  ~a, ~b, ~c,
  #--|--|----
  "x", 3, 12.5,
  "y", 1.2, 0.123
)
# # A tibble: 2 × 3
#   a         b      c
#   <chr> <dbl>  <dbl>
# 1 x       3   12.5  
# 2 y       1.2  0.123
tr <- tribble(
  ~a,  ~b,
  "x", 9:7,
  "y", -3:-1
)
tr
# # A tibble: 2 × 2
#   a     b        
#   <chr> <list>   
# 1 x     <int [3]>
# 2 y     <int [3]>
tr$b[[1]]
# [1] 9 8 7

行名与列的转换

在数据处理的过程中,我们可能会遇到行名包含在数据矩阵中,作为数据的一部分。为了方便计算,但是我们通常需要将行名提取出来,使其独立于数据之外,同时能够作为索引标识来访问数据。

tibble 提供了几个用于操作行名和列名的函数

函数 功能
has_rownames 数据中是否包含行名
remove_rownames 删除行名
rownames_to_column 将行名转换成一个列变量
rowid_to_column 将行号转换成一个列变量
column_to_rownames 将一列转换为行名
head(mtcars)
#                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
# Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
# Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
# Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
# Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
# Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
# Valiant           18.1   6  225 105 2.76 3.460 20.22  1  0    3    1
has_rownames(mtcars)
# [1] TRUE
remove_rownames(mtcars) %>% has_rownames()
# [1] FALSE
rownames_to_column(mtcars, var = "car") %>% 
  as_tibble() %>%
  `[`(1:3, 1:5)
# # A tibble: 3 × 5
#   car             mpg   cyl  disp    hp
#   <chr>         <dbl> <dbl> <dbl> <dbl>
# 1 Mazda RX4      21       6   160   110
# 2 Mazda RX4 Wag  21       6   160   110
# 3 Datsun 710     22.8     4   108    93
rownames_to_column(mtcars, var = "car") %>% 
  column_to_rownames(var = "car") %>%
  `[`(1:3, 1:5)
#                mpg cyl disp  hp drat
# Mazda RX4     21.0   6  160 110 3.90
# Mazda RX4 Wag 21.0   6  160 110 3.90
# Datsun 710    22.8   4  108  93 3.85
iris %>% head(3) %>%
  rowid_to_column()
#   rowid Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1     1          5.1         3.5          1.4         0.2  setosa
# 2     2          4.9         3.0          1.4         0.2  setosa
# 3     3          4.7         3.2          1.3         0.2  setosa

与 data.frame 的区别

tibbledata.frame 之间区别主要在于打印方式和取子集操作。

tibble 只显示前 10 行和列数也是根据屏幕大小自适应,每列列名下面还会显示其数据类型。不同于默认的数据框会在打印大数据集时将整个控制台刷屏,R 的这个毛病真是实力劝退。但有时需要比默认显示更多的输出,下面几个选项可以控制输出格式。

首先,可以显式地使用 print 函数打印数据框并设置行数和列的宽度,width = Inf 表示显示所有列

as_tibble(iris) %>% print(n=5, width=Inf)
# # A tibble: 150 × 5
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#          <dbl>       <dbl>        <dbl>       <dbl> <fct>  
# 1          5.1         3.5          1.4         0.2 setosa 
# 2          4.9         3            1.4         0.2 setosa 
# 3          4.7         3.2          1.3         0.2 setosa 
# 4          4.6         3.1          1.5         0.2 setosa 
# 5          5           3.6          1.4         0.2 setosa 
# # … with 145 more rows

还可以通过设置以下选项来控制默认的打印行为

# 如果多于 n 行,只打印 m 行。
options(tibble.print_max = n, tibble.print_min = m)
# 总是显示所有行
options(tibble.print_min = Inf)
# 无论屏幕的宽度如何,始终打印所有列
options(tibble.width = Inf)

使用$[[ 操作符可以提取数据中的变量

df <- as_tibble(iris)
df$Species %>% head()
# [1] setosa setosa setosa setosa setosa setosa
# Levels: setosa versicolor virginica
df[['Sepal.Length']]
# [1] 5.1 4.9 4.7 4.6 5.0 5.4
df[[1]]
# [1] 5.1 4.9 4.7 4.6 5.0 5.4

使用管道操作

df %>% .$Sepal.Width
# [1] 3.5 3.0 3.2 3.1 3.6 3.9
df %>% .[['Sepal.Width']]
[1] 3.5 3.0 3.2 3.1 3.6 3.9

相较于 data.frametibble 更严格,如果你访问的列不存在,将抛出警告

与旧代码交互

一些比较旧的代码可能不适用于 tibble 数据,这时,我们可以使用 as.data.frame() 将其转换为 data.frame 格式。不须犹豫,直接转换即可。

as.data.frame(df)
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1          5.1         3.5          1.4         0.2  setosa
# 2          4.9         3.0          1.4         0.2  setosa
# 3          4.7         3.2          1.3         0.2  setosa
# 4          4.6         3.1          1.5         0.2  setosa
# 5          5.0         3.6          1.4         0.2  setosa
# 6          5.4         3.9          1.7         0.4  setosa

3. 文件读写

文件读写我们用到的是 readr 包,如果导入了 tidyverse 包则可以直接使用,或者单独导入该包

library(readr)  # version 2.1.0

该包针对不同格式的文件提供了不同的函数,主要包含 7read_ 开头的函数

函数 功能
read_csv 读取 csv 文件
read_csv2 读取分隔符为 ; 的文件
read_tsv 读取分隔符为 \t 的文件,tsv 文件
read_delim 读取固定分隔符的文件
read_fwf 固定宽度的文件
read_table 读取表格文件,列之间用空格隔开
read_log 读取 web 日志文件

这些函数的使用方法基本上是一样的,通常只要提供文件的路径就可以读取文件,而像 read_delim 函数则需要设置额外的分隔符参数,下面我们只以 read_csv 函数来进行说明,比如读取示例文件

mtcars <- read_csv(readr_example("mtcars.csv"))
# Rows: 32 Columns: 11                                                                                                 
# ── Column specification ──────────────────────────────────────────────────────────
# Delimiter: ","
# dbl (11): mpg, cyl, disp, hp, drat, wt, qsec, vs, am, gear, carb
# 
# ℹ Use `spec()` to retrieve the full column specification for this data.
# ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

当你使用 read_csv 时,将会打印每列的列名以及类型,函数自动推断数据的类型,从输出信息可以看到,该分隔符为 ,11 列数据都转换为 double 类型,可以使用 spec 函数获取每列的数据类型

当然在读取文件时,也可以为每列指定类型

mtcars <- read_csv(
  readr_example("mtcars.csv"), 
  col_select = 1:2,     # 只读取前两列,可以是向量或 list,可以是数值索引或列名
  col_types = cols(
    mpg = col_double(),  # 解析为 double
    cyl = col_integer()  # 解析为 integer
    )
)

甚至可以提供一个内联的字符串

read_csv("1,2,3
a,b,c
o,p,q")

这两种读取方式都是默认将第一行作为列名,也可以调整这种模式:

  • 当文件的前几行是数据的描绘信息,并不是你想要的,可以使用 skip = n 跳过前 n 行。或使用 comment = "#" 删除所有以 # 开头的行
read_csv("first line
  second line
  a,b,c
  1,2,3", skip = 2, 
  show_col_types = FALSE  # 不显示列类型信息
)
# # A tibble: 1 x 3
#       a     b     c
#   <dbl> <dbl> <dbl>
# 1     1     2     3
read_csv("# comment line
  a,b,c
  1,2,3", comment = "#", 
  show_col_types = FALSE
)
# # A tibble: 1 x 3
#       a     b     c
#   <dbl> <dbl> <dbl>
# 1     1     2     3
  • 可能你的数据并没有表头,这时可以使用 col_names = FALSE 告诉 read_csv 函数不要把第一行作为表头,然后自动以 X1Xn 依次标注列名
read_csv("1,2,3\n4,5,6", col_names = FALSE)
# # A tibble: 2 x 3
#      X1    X2    X3
#   <dbl> <dbl> <dbl>
# 1     1     2     3
# 2     4     5     6

或者使用 col_names 参数设置列名

read_csv("1,2,3\n4,5,6", col_names = c("x", "y", "z"))
# # A tibble: 2 x 3
#       x     y     z
#   <dbl> <dbl> <dbl>
# 1     1     2     3
# 2     4     5     6

另一个需要调整的选项是 na,用来标识缺省的值,即什么值会被解析为 NA

read_csv("1,2,3\n4,5,*", na = "*", show_col_types = FALSE)
# # A tibble: 1 × 3                                                                                                    
#     `1`   `2` `3`  
#   <dbl> <dbl> <lgl>
# 1     4     5 NA 

其他函数的读取方式也是类似的,就不再做详细介绍了。

相较于 R 自带的 read.csv 等函数 readr 中的函数具有如下优势

  • 读取速度比 R 基础函数快很多(大约 10 倍),在读取时会显示一个进度条,让你知道程序运行到哪里了。

  • 其次,不会自动将字符串转换为 factor

  • 最后,更好的移植,一些基础的 R 函数是与操作系统相关联的

解析向量

在深入了解 readr 如何从磁盘读取文件之前,我们需要稍微讨论一下 parse_* 形式的函数,这些函数使用方式是一样的,第一个参数是要解析的字符向量,而 na 参数指定了哪些字符串应该被视为缺失值

parse_logical(c("TRUE", "FALSE"))
# [1]  TRUE FALSE
parse_integer(c("1", "2", "3"))
# [1] 1 2 3
parse_date(c("2022-05-04", "1949-10-01"))
# [1] "2022-05-04" "1949-10-01"
parse_integer(c("123", "456", "-", "789"), na = "-")
# [1] 123 456  NA 789

如果解析失败,将会抛出一个警告

a <- parse_integer(c("123", "456", "abc", "789"))
# Warning: 1 parsing failure.
# row col               expected actual
#   3  -- no trailing characters    abc

但是错误信息并不会包含在对象的输出中,无法解析的值会转换为 NA

a
# [1] 123 456  NA 789
# attr(,"problems")
# # A tibble: 1 × 4
#     row   col expected               actual
#   <int> <int> <chr>                  <chr> 
# 1     3    NA no trailing characters abc 

如果有许多值解析失败,你可以使用 problems() 来获得完整的信息。返回的还是一个 tibble

problems(a)
# # A tibble: 1 × 4
#     row   col expected               actual
#   <int> <int> <chr>                  <chr> 
# 1     3    NA no trailing characters abc

解析器主要用于处理不同类型的输入,常用的解析器如下

函数 描述
parse_logical 解析逻辑值
parse_integer 解析整数
parse_double 解释浮点数,严格型
parse_number 灵活型数字解析器
parse_character 解析字符串
parse_factor 解析因子
parse_datetimeparse_dateparse_time 解析日期

解析器

对数值的解析看起来挺简单的,但其实有几个问题需要解决:

  1. 不同地区的人书写数字的方式会有所不同,一些国家使用 . 来区分实数的整数部分和小数部分,而有些国家使用 , 来区分

  2. 数字通常还带有单位符,比如 45%$10

  3. 而且较大的数字通常还包含分组字符,例如 10,000

为了解决第一个问题,可以使用 locale 函数来指定不同地区的解析方式,通过指定 decimal_mark 参数的值来覆盖默认的小数点的字符 .

parse_double("3.1415")
# [1] 3.1415
parse_double("3,1415", locale = locale(decimal_mark = ","))
# [1] 3.1415

parse_number 函数用于解决第二个问题,它会忽略数字前后的非数字字符,这对货币和百分比数字特别有用,同时也适用于提取文本中嵌入的数字

parse_number("$10")
# [1] 10
parse_number("45%")
# [1] 45
parse_number("I have $1234567890")
# [1] 1234567890

组合 parse_numberlocale 来忽略分组标记就可以解决最后一个问题

parse_number("123,456,789")
# [1] 123456789
parse_number("123.456.789", locale = locale(grouping_mark = "."))
# [1] 123456789
parse_number("123'456'789", locale = locale(grouping_mark = "'"))
# [1] 123456789

parse_character 来解析字符串看起来应该是很简单的,能够直接把输入返回。但是,生活并不是看起来那么简单的,因为同一个字符串可以有不同的编码方式。readr 的读写默认都是使用 UTF-8 编码,如果你的系统不支持 UTF-8 格式(你的机子不会老到这种程度的),你看到的将是一堆乱码。看个例子

(a <- "je ne connais pas le fran\xe7ai")
# [1] "je ne connais pas le fran\xe7ai"
(b <- "\xd7\xe1\xdf\xf1\xe5\xf4\xe5")
# [1] "\xd7\xe1\xdf\xf1\xe5\xf4\xe5"

非英文字符并未被正确转译,使用 parse_character 并指定编码格式,可以正确解析字符串

parse_character(a, locale = locale(encoding = "Latin1"))
# [1] "je ne connais pas le françai"
parse_character(b, locale = locale(encoding = "iso-8859-7"))
# [1] "Χαίρετε"

当我们不知道字符串的编码方式时,可以使用 guess_encoding 函数来推断编码方式,它的第一个参数可以是文件的路径,也可以是原始字符串向量。但它也并不能保证万无一失,当你有文本数据量较大时效果会更好,也可以自己手动尝试几种常用的编码。

guess_encoding(charToRaw(a))
# # A tibble: 2 × 2
#   encoding   confidence
#   <chr>           <dbl>
# 1 ISO-8859-1       0.98
# 2 ISO-8859-2       0.51
guess_encoding(charToRaw(b))
# # A tibble: 0 × 2
# # … with 2 variables: encoding <chr>, confidence <dbl>

parse_factor 函数用于解析因子,为 levels 参数传入一个已知的向量作为分类值,当出现分类范围外的值时会抛出警告信息

sex <- c("male", "formale")
parse_factor(c("male", "formale", "male"), levels = sex)
# [1] male    formale male   
# Levels: male formale

时间和日期的解析可以从下面三个解析器中选择

  • parse_datetime 用于解析时间日期,默认日期的组成部分按年、月、日、时、分、秒 排列,也可以自己指定格式
parse_datetime("2022-05-01T13:14")
# [1] "2022-05-01 13:14:00 UTC"
parse_datetime("2022-05-01")
# [1] "2022-05-01 UTC"
parse_datetime("01/01/2010", format = "%d/%m/%Y")
# [1] "2010-01-01 UTC"
  • parse_date() 用于解析日期,默认的形式 yyyy-mm-ddyyyy/mm/dd
parse_date("2022-03-04")
# [1] "2022-03-04"
parse_date("2022/03/04")
# [1] "2022-03-04"
parse_date("2022 03 04", format = "%Y %m %d")
# [1] "2022-03-04"
  • parse_time() 用于解析时间,默认形式为 hh:mm(:ss am/pm)
parse_time("01:10 pm")
# 13:10:00
parse_time("10:10:10")
# 10:10:10
parse_time("13:14:15 US/Central", format = "%H:%M:%S %Z")
# 13:14:15

日期时间的格式,可以由以下几种格式进行组成

符号 含义 符号 含义
%Y 4 位数字的年份 %y 2 位数字的年份
%m 2 位数字的月份 %b 月份简写,如 Jan
%B 月份全称,如 January %d 一个月内的 2 位数字的天数
%H 24 小时制 %I 12 小时制,必须包含 %p
%p AM/PM %M 2 位数字的分钟数
%S 整数秒 %OS 实数秒
%Z 时区 %z 相对于标准时间的偏移,如 +0800
%. 跳过一个非数字字符 %* 跳过任意个非数字字符

解析文件

讲到这里,你应该已经很清楚怎么解析单个向量了,现在让我们回到上面提到的 readr 是如何解析文件的问题。

readr 通过读取数据的前 1000 行,并使用一些启发式方法来推断出每一列的类型,我们可以使用 guess_parser 来推测列数据类型,

guess_parser(c("1", "3", "5"))
# [1] "double"
guess_parser("1561,123")
# [1] "number"
guess_parser("2022-05-01")
# [1] "date"
guess_parser("13:14")
# [1] "time"
guess_parser(c("TRUE", "FALSE"))
# [1] "logical"
> 
> str(parse_guess("2010-10-10"))
 Date[1:1], format: "2010-10-10"

使用 parse_guess 函数可以用推测到的最优类型去解析数值

guess_parser(c("2022-02-20"))
# [1] "date"
parse_guess(c("2022-02-20"))
# [1] "2022-02-20"

启发式方法将尝试以下类型解析规则,如果所有规则都不适用,则保持为字符串格式

类型 规则
logical 只包含 F, T, FALSE, 或 TRUE
integer 只包含数字和符号 -
double 只包含有效双精度数字
number 相较于 double,多了分组标记
time 匹配默认的 time_format
date 匹配默认的 date_format
date-time 任何 ISO8601 格式

启发式方法并不总是都适用,特别是对于大数据文件而言,存在一种较为极端的情况,就是前 1000 行与后面的数据类型不匹配的问题。例如,可能有一个双精度的列,但它的前 1000 行中只包含整数,或者列中包含许多缺失值,如果前 1000 行只包含 NAreadr 会猜测它是一个逻辑向量,但这可能并不是我们所想要的结果

在这时,可能需要我们对数据进行观测,手动为每一列设置类型,例如

mtcars <- read_csv(
  readr_example("mtcars.csv"), 
  col_select = 1:2,     # 只读取前两列,可以是向量或 list,可以是数值索引或列名
  col_types = cols(
    mpg = col_double(),  # 解析为 double
    cyl = col_integer()  # 解析为 integer
    )
)

每个 parse_* 函数都有一个对应的 col_* 函数,在读取文件时将其传入到 col_types 参数来指定解析方式。

还有其他一些通用策略可以帮助您解析文件,例如

  • 设置最大猜测行数 guess_max

  • 或者设置默认解析为字符串,然后使用 type_convert 进行类型推断

  • 在解析大文件时,通过设置 n_max 参数为一个较小的值,每次读取一点可以消除常见问题,同时加速您的迭代

  • 或者使用 read_lines 每次读取一行

写入文件

readr 提供了几个以 write_* 形式的函数用于将数据写入到文件中,如 write_csvwrite_tsv。如果要保存为 Excel 格式的 csv 文件,可以使用 write_excel_csv()

这些函数主要有两个参数需要设置,即数据和文件路径,你还可以为 na 参数指定一个字符串,表示缺失值写入到文件中的值,以及是否想要追加到现有文件

write_csv(mtcars, "mtcars.csv")

注意: 在保存到 csv 时,类型信息会丢失,这使得 csv 在缓存中间结果时有点不可靠。当需要类型信息时,可以使用其他两种方法

  • write_rdsread_rds 是对基本函数 readRDSsaveRDS 的封装,将数据存储为 R 自定义的 RDS 二进制
write_rds(mtcars, "mtcars.rds")
read_rds("mtcars.rds")
# # A tibble: 32 × 2
#      mpg   cyl
#    <dbl> <int>
#  1  21       6
#  2  21       6
#  3  22.8     4
#  4  21.4     6
#  5  18.7     8
#  6  18.1     6
#  7  14.3     8
#  8  24.4     4
#  9  22.8     4
# 10  19.2     6
# # … with 22 more rows
  • 使用feather 包将数据输出为一种可以跨编程语言共享的快速读写的二进制文件格式,featherRDS 更快,且可以独立于 R 使用
library(feather)
write_feather(mtcars, "mtcars.feather")
read_feather("mtcars.feather")
# # A tibble: 32 × 2
#      mpg   cyl
#    <dbl> <int>
#  1  21       6
#  2  21       6
#  3  22.8     4
#  4  21.4     6
#  5  18.7     8
#  6  18.1     6
#  7  14.3     8
#  8  24.4     4
#  9  22.8     4
# 10  19.2     6
# # … with 22 more rows

4. dplyr 基础

通常,我们读取的数据不可能完全满足我们后续的分析需求,因此需要对读取进来的数据进行处理。比如,创建一些新的变量,或者只是想重命名变量或重新排列数据观察结果,以便使数据更易于使用。在本节及后续几节,我们将介绍如何使用 tidyverse 中的 dplyr 包来对数据进行操作。

先导入包

library(dplyr)  # version 1.0.7
# library(tidyverse)

dplyr 中某些函数与 R 中的某些基础函数冲突,为了避免函数之间的混淆,可以使用包名加函数名的方式来使用,例如 stats::filter

我们的重点是介绍 dplyr 包的使用,为了探索 dplyr 的基本数据操作,我们将使用 nycflights13 包中的 flights 数据,此数据包含 2013 年从纽约起飞的所有 336776 架次航班,这些数据来自美国交通统计局。

# 安装 nycflights13 包,版本为 1.0.2
install.packages("nycflights13")
# 导入
library(nycflights13)

flights 是一个 tibble 类型,每架航班包含 19 列信息

dim(flights)
# [1] 336776     19
colnames(flights)
#  [1] "year"           "month"          "day"            "dep_time"      
#  [5] "sched_dep_time" "dep_delay"      "arr_time"       "sched_arr_time"
#  [9] "arr_delay"      "carrier"        "flight"         "tailnum"       
# [13] "origin"         "dest"           "air_time"       "distance"      
# [17] "hour"           "minute"         "time_hour" 
class(flights)
# [1] "tbl_df"     "tbl"        "data.frame"

我们首先介绍几个比较重要的 dplyr 动词函数,在不同的条件它们还有许多变种,这些函数可帮助您解决绝大多数的数据处理难题。它们的工作方式相似,都是接受一个数据框,然后针对变量进行不同的操作,最后返回一个新的数据框。将这些属性搭配在一起使用,能够以一个轻松的步骤来完成复杂的操作,动词函数主要有

函数 功能 函数 功能
filter 逻辑条件过滤行 slice 按照位置获取行
distinct 行数据去重 arrange 行数据排序
select 列选择 pull 将列值转换为向量
mutate 添加新列 transmute 只保留添加的新列
rename 列重命名 summarise 汇总函数
add_row 添加行 count 统计唯一值的数量

行过滤(filter

filter 函数用于根据规则对行数据进行过滤筛选子集,与内置 subset 函数类似,主要是如何编写过滤规则。

过滤规则的编写也是非常简单的,直接使用变量名称(列名)进行运算即可,也可以使用逻辑运算符将多个条件合并在一起,你会发现这与内置的判断没啥区别。例如,我们可以通过以下方式查询 20133 月的航班数量

filter(flights, year == 2013, month == 3) %>% nrow()
# [1] 28834

dplyr 函数不会修改其输入,且都会返回一个新的数据,因此如果要保存结果,则需要使用赋值运算符

a <- filter(flights, year == 2013, month == 3) %>% nrow()

注意,上面的筛选参数用的是 == 而不是 =,如果你用错了将会抛出异常

对于浮点数,由于精度的问题,有时候可能不太适合 == 来判断数值相等,例如

sqrt(3) ^ 3 == 3
# [1] FALSE

计算机的精度是有限的,并不能存储一个无限的数值,但通常可以用 near() 函数代替 ==,来作近似判断

near(sqrt(3) ^ 2,  3)
# [1] TRUE

多个变量的过滤表示逻辑与,两个条件均满足的数据进行过滤,而对于其他类型的组合,可以使用布尔操作符:& | !。例如,查询 20133-4 月之间的航班数

filter(flights, year == 2013, month == 3 | month == 4) %>% nrow()
# [1] 57164
filter(flights, year == 2013, month %in% c(3, 4) ) %>% nrow()  # 成员判断
# [1] 57164

注意,该过滤条件不能写成

filter(flights, year == 2013, month == (3 | 4))

因为 3 | 4 返回的是 TRUE,而 TRUE 的值等于 1,所以这个表达式的结果返回的将是 20131 月的所有航班数

对于 NA 值的过滤也是一样的

tibble(x = c(1, NA, 3)) %>% filter(!is.na(x))
# # A tibble: 2 × 1
#       x
#   <dbl>
# 1     1
# 2     3

与内置的条件判断基本一样,不需要更多的说明了。

行去重(distinct

对行数据去重,可以使用 distinct 函数

df <- tibble(
  g = c(1, 1, 2, 2),
  x = c(1, 1, 2, 1)
) 
df %>% distinct(df)
# # A tibble: 3 × 2
#       g     x
#   <dbl> <dbl>
# 1     1     1
# 2     2     2
# 3     2     1
distinct(df, x)
# # A tibble: 2 × 1
#       x
#   <dbl>
# 1     1
# 2     2

行子集(slice

该函数可以根据行索引来获取数据子集,可以用于对列进行获取、删除以及去重,该函数还有几个用于不同情况下的变种

  • slice_headslice_tail 获取首尾数据
  • slice_sample 随机提取行,取代之前的 sample_nsample_frac
  • slice_minslice_max 用于提取最小值和最大值所在的行,取代之前的 top_ntop_frac
slice(flights[,1:5], 1)
# # A tibble: 1 × 5
#    year month   day dep_time sched_dep_time
#   <int> <int> <int>    <int>          <int>
# 1  2013     1     1      517            515
slice_head(flights[,1:5], n = 3)
# # A tibble: 3 × 5
#    year month   day dep_time sched_dep_time
#   <int> <int> <int>    <int>          <int>
# 1  2013     1     1      517            515
# 2  2013     1     1      533            529
# 3  2013     1     1      542            540
slice_sample(flights[,1:5], n = 3)
# # A tibble: 3 × 5
#    year month   day dep_time sched_dep_time
#   <int> <int> <int>    <int>          <int>
# 1  2013    12    29     1527           1529
# 2  2013    11     9     1653           1700
# 3  2013     1    23      854            855
flights[
  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

名本无名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值