数据处理非常重要:没有这个过程,就无法使用数据进行工作!数据处理包括三个重要环节。
- 导入
- 整理
- 转换
使用tibble实现简单数据框 介绍数据框的一种变体:tibble。你将会了解 tibble 和普通数据框的区别,以及如何“手工构造”tibble。
使用readr进行数据导入 介绍如何从磁盘读取数据并导入 R。重点关注矩阵格式的纯文本文件,但会给出一些指示,表明哪些包可以处理其他类型的数据。
使用dplyr处理关系数据 介绍一些工具,以处理具有多种相关关系的数据集。
使用stringr处理字符串 介绍正则表达式,即处理字符串的一种强大工具。
使用forcats处理因子 展示 R 如何保存分类数据。当一个变量的取值范围是有限集合,或者我们不想按字母顺序排列字符串时,就可以使用分类数据。
*使用lubridate处理日期和时间 介绍处理日期和日期时间型数据的核心工具。
使用tibble实现简单数据框
tibble 是一种简单数据框,它对传统数据框的功能进行了一些修改,以便更易于使用。
R 是一门古老的语言,其中有些功能在10年或20年前是适用的,但现在已经过时。
在不破坏现有代码的前提下,很难修改 R的基础功能,很多革新都是以拓展包的方式出现的。
tibble 包所提供的简单数据框更易于 tidyverse 中使用。多数情况下,我们会交替使用 tibble 和 数据框,如果想要特别强调 R 内置的传统数据框,会使用 data.frame 来表示。
library(tibble)
Warning message:
"package 'tibble' was built under R version 4.0.5"
本书中使用的所有函数几乎都可以创建tibble,因为tibble是tidyverse的标准功能之一。
由于多数其他R包使用的是标准数据框,可以使用 as_tibble()
函数来完成转换:
as_tibble(iris)[1:2,]
Sepal.Length | Sepal.Width | Petal.Length | Petal.Width | Species |
---|---|---|---|---|
<dbl> | <dbl> | <dbl> | <dbl> | <fct> |
5.1 | 3.5 | 1.4 | 0.2 | setosa |
4.9 | 3.0 | 1.4 | 0.2 | setosa |
可以使用 tibble()
函数使用一个向量来创建 tibble。tibble() 会自动重复长度为 1 的输入,并可以使用刚刚创建的新变量。
tibble(
x = 1:5,
y = 1,
z = x^2 + y
)
x | y | z |
---|---|---|
<int> | <dbl> | <dbl> |
1 | 1 | 2 |
2 | 1 | 5 |
3 | 1 | 10 |
4 | 1 | 17 |
5 | 1 | 26 |
相比于 data.frame,tibble()
函数的功能少得多:
- 不能改变输入的类型(如,不能将字符串转换为因子)、变量的名称
- 不能创建行名称
创建 tibble 的另一种方法是使用 tribble()
函数,tribble 是 transposed tibble(转置 tibble)的缩写。tribble() 是定制化的,可以对数据按行进行编码:列标题由公式(以 ~
开头)定义,数据条目以逗号分隔,这样就可以用易读的方式对少量数据进行布局。
tribble(
~x, ~y, ~z,
#--|--|---- 可选,这样写能明确指出标题行的位置
"a",2,3,
"b",1,8
)
x | y | z |
---|---|---|
<chr> | <dbl> | <dbl> |
a | 2 | 3 |
b | 1 | 8 |
对比 tibble 与 data.frame
tibble 和传统 data.frame 的使用方法主要有两处不同:打印和取子集。
打印
tibble 打印方法进行了优化,只显示前 10 行结果(Rstudio中,在 jupyter 中会全部显示),并且列也是适合屏幕的,这种方式非常适合大数据集。除了打印列名,tibble 还会打印出列的类型。这个功能借鉴于str()
函数。
tibble(
a = lubridate::now() + runif(1e3) * 86400,
b = lubridate::today() + runif(1e3) * 30
)
取子集
$
(按名称提取变量) 和 [[
(可以按名称或位置提取变量)
df <- tibble(
x = runif(5),
y = rnorm(5))
# 按名称提取
df$x
df[['y']]
- 0.976251509739086
- 0.131041357060894
- 0.876212562201545
- 0.795781070599332
- 0.97147882077843
- -0.822334674490723
- 0.457416425847197
- 0.764743684096069
- 0.190165831982873
- 0.147661380523245
#按位置提取
df[[1]]
- 0.976251509739086
- 0.131041357060894
- 0.876212562201545
- 0.795781070599332
- 0.97147882077843
如果使用管道,需要借用一个占位符.
:
df %>% .$x
- 0.976251509739086
- 0.131041357060894
- 0.876212562201545
- 0.795781070599332
- 0.97147882077843
使用 readr 进行数据导入
这里学习使用 tidyverse 中的 readr 包进行导入。
readr 的多数函数用于将平面文件转换为数据框
read_csv()
读取逗号分隔文件、read_csv2()
读取分号分隔文件、read_tsv()
读取制表符分隔文件、read_delim()
可以读取使用任意分隔符的文件read_fwf()
读取固定宽度的文件,既可以使用 fwf_widths() 函数宽度设定域,也可以使用fwf_position()
函数按照位置来设定域。read_table()
读取固定宽度文件的一种常用变体,其中使用空白字符来分隔列。read_log()
读取 Apache 风格的文件。
这些函数都有同样的语法
这里主要介绍 read_csv()
因为熟悉基础包中 read.table,并且暂未有不适症状,所以目前这一部分略过了。
使用 dplyr 处理关系数据
这一部分已在 part1 探索部分谈到,请跳转R数据科学,第一部分:探索
使用stringr处理字符串
本章重点是正则表达式。这里的正则表达式,和其他命令有很多共同之处,如 awk
、grep
,所以如果你掌握了其他的正则表达式,会轻松很多。
suppressMessages(library(tidyverse))
suppressMessages(library(stringr))
字符串基础
'
和"
没有区别- 用反斜杠
\
进行转义 - 使用
?'"'
或?"'"
查看完整的特殊字符列表 - 多个字符串通常保存在一个字符向量中,可以使用
c()
函数来创建字符向量
字符串长度
R 基础包中包含了很多字符串处理函数,但尽量不使用这些函数,因为它们的使用方法都不一致,很难记忆。相反,使用 stringr
中的函数,这些函数的名称更直观,都是以 str_
开头的。例如,str_length()
函数可以返回字符串中的字符数量:
str_length(c('a','R for data science', NA))
- 1
- 18
- <NA>
并且大多数都是向量化的
字符串组合
想要组合两个或更多字符串,可以使用 str_c()
函数:
str_c("x", "y", sep = ' ')
‘x y’
字符串取子串
使用 str_sub()
函数来提供字符串的一部分。除了字符串参数外,还有 start 和 end 参数,它们给出了子串的位置(包括 start 和 end 在内,符数表示从后往前数):
x <- c('apple','banana','pear')
str_sub(x, 1, 3)
- 'app'
- 'ban'
- 'pea'
可以通过赋值形式修改字符串
str_sub(x, 1, 1) <- str_to_upper(str_sub(x,1,1))
x
- 'Apple'
- 'Banana'
- 'Pear'
使用正则表达式进行模式匹配
通过str_view()
和 str_view_all()
函数来学习正则表达式,这两个函数接受一个字符向量和一个正则表达式,并显示出它们是如何匹配的。
基础匹配
x <- c('apple','banana','pear')
# 精确匹配
str_view(x,'an')
.
匹配任意字符(除了换行符)
str_view(x, '.a.')
为了匹配字符.
,需要一个\
进行转义。
这样有个问题:因为使用字符串表示正则表达式,而且 \
在字符串中也用作转义字符,所以正则表达式\.
的字符串形式应该是\\.
。
更复杂的是,如何匹配\
这个字符呢?还是要去除\
本身的特殊意义,建立形式为\\
的表达式。实际上,匹配一个\
,需要四个反斜杠\\\\
。
实际上就是第一个\
转义了第二个\
,告诉正则表达式这里确实是一个\
,而不是一个转义字符
x <- 'a\\b'
writeLines(x)
str_view(x, '\\\\')
a\b
锚点
^
表示开头$
表示结尾
字符类与字符选项
\d
匹配任意数字\s
匹配任意空白字符(如空格、制表符和换行符)[abc]
可以匹配 a、b 或 c[^abc]
可以匹配除 a、b、c以外的任意字符
请牢记,要想创建包含 \d
或 \s
的正则表达式,你需要在字符串中对\
转义,因此需要输入\\d
或\\s
你还可以使用字符选项创建多个可选模式。例如,abc|d..f 可以匹配 abc 或 deaf。
❗注意:因为 |
的优先级很低,所以 abc|xyz 匹配的是 abc 或 xyz,而不是 abcyz 或 adxyz。与数据表达式一样,如果优先级令人困惑,可以使用括号使表达式更清晰一些:
str_view(c('grey','gray'), 'gr(e|a)y')
重复
?
0次或1次+
1次或多次*
0次或多次
注意这些运算符的优先级非常高,因此使用 colou?r 既可以匹配 color,也可以匹配 colour。这意味着很多时候需要使用括号,比如 bana(na)+。
还可以精确设置匹配次数:
{n}
匹配n次{n,}
匹配n次或更多次{,m}
最多匹配m次{n, m}
匹配n到m次
x = "1888 is the longest year in Roman numerals: MDCCCLXXXVIII"
str_view(x, "C{2}")
str_view(x, "C{2,}")
str_view(x, "C{2,3}")
默认的匹配方式是“贪婪的”:正则表达式会匹配尽量长的字符串。通过在正则表达式后面添加一个?
,将匹配方式改为“懒惰的”,及匹配尽量短的字符串。
str_view(x, "C{2,3}?")
str_view(x, "C[LX]+?")
分组与回溯引用
括号除了用于消除歧义,还可以定义“分组”,你可以通过“回溯引用”(如\1、\2等)来引用这些分组。例如,以下的正则表达式可以找出名称中有重复的一堆字母的所有水果:
str_view(fruit, "(..)\\1",match = TRUE)
工具
- 确定与某种模式相匹配的字符串
- 找出匹配的位置
- 提取出匹配的内容
- 使用新值替换匹配内容
- 基于匹配拆分字符串
匹配检测
想要确定一个字符向量能否匹配一种模式,可以使用 str_detect()
函数。它返回一个与输入向量具有同样长度的逻辑向量:
x <- c("apple", "banana", "pear")
str_detect(x, "e")
- TRUE
- FALSE
- TRUE
记住,从数学意义上来说,逻辑向量中的 FALSE 为 0,TRUE 为 1.这使得在匹配特别大的向量时,sum() 和 mean() 函数能够发挥更大的作用
# 有多少个以 t 开头的常用单词
sum(str_detect(words, "^t"))
65
# 以元音字母结尾的常用单词的比例是多少
mean(str_detect(words, "[aeiou]$"))
0.276530612244898
当逻辑条件非常复杂时(如,匹配 a 或 b,但不匹配 c,除非 d 成立),一般来说,相对于创建单个正则表达式,使用逻辑运算符将多个 str_detect() 调用组合起来会更容易。
例如,以下两种方法均可找出不包含原因字母的所有单词:
# 找出至少包含一个元音字母的所有单词,然后取反
no_vowels_1 <- !str_detect(words, "[aeiou]")
# 找出仅包含辅音字母的所有单词
no_vowels_2 <- str_detect(words, "^[^aeiou]+$")
identical(no_vowels_1, no_vowels_2)
TRUE
两种方法的结果是一样的,但第一种方法明显更容易理解。如果正则表达式过于复杂,则应该将其分解为几个更小的子表达式,将每个子表达式的匹配结果赋值给一个变量,并使用逻辑运算组合起来
字符串通常是数据框的一列,也可以使用filter 操作
df <- tibble(
word = words,
i = seq_along(word)
)
df %>% filter(str_detect(words, "x$"))
word | i |
---|---|
<chr> | <int> |
box | 108 |
sex | 747 |
six | 772 |
tax | 841 |
str_count()
返回字符串中匹配的数量
x <- c("apple", "banana", "pear")
str_count(x, "a")
- 1
- 3
- 1
注意匹配从来不会重叠。例如在”abababa”中,模式”aba”会匹配多少次?正则表达式会告诉你是2次,而不是3次:
str_count("abababa", "aba")
2
str_view_all("abababa", "aba")
很多 stringr 函数都是成对出现的: 一个函数用于单个匹配,另一个函数用于全部匹配,后者会有后缀 _all
提取匹配内容
想要提取匹配的实际文本,我们可以使用 str_extract()
函数。
使用维基百科上的 Harvard sentences,这个数据集是用来测试 VOIP 系统的,但也可以用来练习正则表达式。
length(sentences)
720
head(sentences)
- 'The birch canoe slid on the smooth planks.'
- 'Glue the sheet to the dark blue background.'
- 'It\'s easy to tell the depth of a well.'
- 'These days a chicken leg is a rare dish.'
- 'Rice is often served in round bowls.'
- 'The juice of lemons makes fine punch.'
假设我们想要找出包含一种颜色的所有句子。首先,创建一个颜色名称向量,然后转为一个正则表达式:
colors <- c("red","orange", "yellow", "green", "blue", "purple")
color_match <- str_c(colors, collapse = "|")
color_match
‘red|orange|yellow|green|blue|purple’
has_color <- str_subset(sentences, color_match)
matches <- str_extract(has_color, color_match)
head(matches)
- 'blue'
- 'blue'
- 'red'
- 'red'
- 'red'
- 'blue'
注意:str_extract()
只会提取第一个匹配。使用 str_extract_all() 得到所有匹配,它会返回一个列表;如果设置了 simplify = TRUE,那么 str_extract_all() 会返回一个矩阵,其中较短的匹配会拓展到与最长的匹配具有同样的长度:
head(str_extract_all(has_color, color_match, simplify = TRUE),5)
blue | |
blue | |
red | |
red | |
red |
分组匹配
我们在本章前面讨论了括号在正则表达式中的用法,它可以阐明优先级,还能对正则表达式进行分组,分组可以在匹配时回溯引用。你还可以使用括号来提取一个复杂匹配的各个部分。举例来说,假设我们想从句子中提取出名词。
我们先进行一种启发式实验,找出跟在 a 或 the 后面的所有单词。
因为使用正则表达式定义“单词”有一点难度,所以我们使用简单的近似定义,至少有1个非空格字符的字符序列:
noun <- "(a|the) ([^ ]+)"
sentences %>%
str_subset(noun) %>%
head(10) %>%
str_extract(noun)
- 'the smooth'
- 'the sheet'
- 'the depth'
- 'a chicken'
- 'the parked'
- 'the sun'
- 'the huge'
- 'the ball'
- 'the woman'
- 'a helps'
替换匹配内容
str_replace()
和 str_replace_all()
通过提供一个命名向量,使用 str_replace_all()
函数可以同时执行多个替换:
x <- c("1 house", "2 cars", "3 people")
str_replace_all(x, c("1" = "one", "2" = "two", "3" = "three"))
- 'one house'
- 'two cars'
- 'three people'
除了使用固定字符串替换匹配内容,你还可以使用回溯引用来插入匹配中的分组。在下面的代码中,我们交换了第二个单词和第三个单词的顺序:
sentences %>%
str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\\1 \\3 \\2") %>%
head
- 'The canoe birch slid on the smooth planks.'
- 'Glue sheet the to the dark blue background.'
- 'It\'s to easy tell the depth of a well.'
- 'These a days chicken leg is a rare dish.'
- 'Rice often is served in round bowls.'
- 'The of juice lemons makes fine punch.'
拆分
str_split()
函数可以按字符串拆分为多个片段。例如,我们可以将句子拆分成单词:
sentences %>% head(5) %>% str_split(" ")
-
- 'The'
- 'birch'
- 'canoe'
- 'slid'
- 'on'
- 'the'
- 'smooth'
- 'planks.'
-
- 'Glue'
- 'the'
- 'sheet'
- 'to'
- 'the'
- 'dark'
- 'blue'
- 'background.'
-
- 'It\'s'
- 'easy'
- 'to'
- 'tell'
- 'the'
- 'depth'
- 'of'
- 'a'
- 'well.'
-
- 'These'
- 'days'
- 'a'
- 'chicken'
- 'leg'
- 'is'
- 'a'
- 'rare'
- 'dish.'
-
- 'Rice'
- 'is'
- 'often'
- 'served'
- 'in'
- 'round'
- 'bowls.'
因为字符向量的每个分量会包含不同数量的片段,所以 str_split() 会返回一个列表。如果你拆分的是长度为1的向量,那么只要简单地提取列表地第一个元素即可:
"a|b|c|d" %>% str_split("\\|") %>%
.[[1]]
- 'a'
- 'b'
- 'c'
- 'd'
和其他 stringr 函数一样,可以设置 simplify = TRUE 返回一个矩阵:
sentences %>%
head(5) %>%
str_split(" ", simplify = TRUE)
The | birch | canoe | slid | on | the | smooth | planks. | |
Glue | the | sheet | to | the | dark | blue | background. | |
It's | easy | to | tell | the | depth | of | a | well. |
These | days | a | chicken | leg | is | a | rare | dish. |
Rice | is | often | served | in | round | bowls. |
除了模式,还可以通过字母、行、句子和单词边界(boundary()
函数)来拆分字符串:
x <- "this is a sentence. this is another sentence."
str_view_all(x, boundary("word"))
定位匹配内容
str_locate()
和 str_locate_all()
函数可以给出每个匹配的开始位置和结束位置。当没有其他函数能够精确地满足需求时,这两个函数特别有用。
其他类型的模式
当使用一个字符串作为模式时,R会自动调用 regex()
函数对齐进行包装:
# 正常调用:
str_view(fruit, “nana”)
# 上面是以下形式的简写
str_view(fruit, regex(“nana”))
可以使用 regex() 函数的其他参数来控制具体的匹配方式。
- ignore_case = TRUE
- multiline = TRUE 可以使得 ^ 和 $ 从每行的开头和末尾开始匹配,而不是完整字符串的开头和末尾开始匹配
- comments = TRUE 可以让你在复杂的正则表达式中加入注释和空白字符,以便容易理解。匹配时会忽略空格 和 # 后面的内容。如果想要匹配一个空格,需要对其进行转义:”\ “
- dotall = TRUE 可以使得
.
匹配 \n 在内的所有字符
phone <- regex("
\\(? # 可选的开头括号
(\\d{3}) # 地区编码
[)- ]? # 可选的闭括号、短划线或空格
(\\d{3}) # 另外3个数字
",comments = TRUE)
str_match("514-791-456", phone)
514-791 | 514 | 791 |
除了 regex()
,还可以使用其他函数
fixed()
按照字符串的字节形式进行精确匹配,它会忽略正则表达式中的所有特殊字符,并在非常低的层次上进行操作。这样可以让你不用进行那些复杂的转义操作,而且速度比普通正则表达式要快很多。- 可以用
boundary()
函数来匹配边界。
正则表达式的其他应用
R基础包中有两个常用函数,它们也可以使用正则表达式。
apropos()
可以在全局环境中搜索所有可用对象。当不能确切想起函数名称时,这个函数特别有用:
apropos("replace")
- '%+replace%'
- 'replace'
- 'replace_na'
- 'setReplaceMethod'
- 'str_replace'
- 'str_replace_all'
- 'str_replace_na'
- 'theme_replace'
- dir() 函数可以列出一个目录下的所有文件。
dir()
函数的pattern参数可以是一个正则表达式,此时它只返回与这个模式相匹配的文件名。
dir(pattern = "\\.Rmd$")
使用forcats处理因子
因子(又称分类变量)
library(tidyverse)
library(forcats)
创建因子
month_levels <- c("Jan", "Feb", "Mar","Apr","May","Jun", "Jul","Aug","Sep","Oct","Nov","Dec")
x1 <- c( "Apr","May","Oct","Nov","Jan")
y1 <- factor(x1, levels = month_levels)
y1
- Jan
- Apr
- May
- Oct
- Nov
Levels:
- 'Jan'
- 'Feb'
- 'Mar'
- 'Apr'
- 'May'
- 'Jun'
- 'Jul'
- 'Aug'
- 'Sep'
- 'Oct'
- 'Nov'
- 'Dec'
不在有效水平集合内的所有值都会自动转换为 NA:
x2 <- c("Jam", "Apr","May","Oct","Nov")
factor(x2, levels = month_levels)
- <NA>
- Apr
- May
- Oct
- Nov
Levels:
- 'Jan'
- 'Feb'
- 'Mar'
- 'Apr'
- 'May'
- 'Jun'
- 'Jul'
- 'Aug'
- 'Sep'
- 'Oct'
- 'Nov'
- 'Dec'
有时你会想让因子的顺序与初始数据的顺序一致。在创建因子时,将水平设置为 unique(x),或者在创建因子后再对其使用 fct_inorder()
函数,就可以达到这个目的:
f1 <- factor(x1, levels = unique(x1))
f1
- Apr
- May
- Oct
- Nov
- Jan
Levels:
- 'Apr'
- 'May'
- 'Oct'
- 'Nov'
- 'Jan'
f2 <- x1 %>% factor() %>% fct_inorder()
f2
- Apr
- May
- Oct
- Nov
- Jan
Levels:
- 'Apr'
- 'May'
- 'Oct'
- 'Nov'
- 'Jan'
# 访问因子的有效水平集合
levels(f2)
- 'Apr'
- 'May'
- 'Oct'
- 'Nov'
- 'Jan'
修改因子水平
比修改因子水平顺序更强大的操作是修改水平的值。最常用、最大强的工具是 fct_recode()
函数,它可以对每个水平进行修改或重新编码。
gss_cat %>% count(partyid)
partyid | n |
---|---|
<fct> | <int> |
No answer | 154 |
Don't know | 1 |
Other party | 393 |
Strong republican | 2314 |
Not str republican | 3032 |
Ind,near rep | 1791 |
Independent | 4119 |
Ind,near dem | 2499 |
Not str democrat | 3690 |
Strong democrat | 3490 |
gss_cat %>% mutate(partyid = fct_recode(partyid,
"No response" = "No answer",
"No idea" = "Don't know"
)) %>% count(partyid)
partyid | n |
---|---|
<fct> | <int> |
No response | 154 |
No idea | 1 |
Other party | 393 |
Strong republican | 2314 |
Not str republican | 3032 |
Ind,near rep | 1791 |
Independent | 4119 |
Ind,near dem | 2499 |
Not str democrat | 3690 |
Strong democrat | 3490 |
fct_recode() 会让没有明确提及的水平保持原样,如果一不小心修改了一个不存在的水平,它会给出警告。
可以将多个原水平赋给同一个新水平,这样就可以合并原来的分类:
gss_cat %>% mutate(partyid = fct_recode(partyid,
"Ind" = "Ind,near rep",
"Ind" = "Independent",
"Ind" = "Ind,near dem"
)) %>% count(partyid)
partyid | n |
---|---|
<fct> | <int> |
No answer | 154 |
Don't know | 1 |
Other party | 393 |
Strong republican | 2314 |
Not str republican | 3032 |
Ind | 8409 |
Not str democrat | 3690 |
Strong democrat | 3490 |
使用这种操作时一定要小心,如果合并了原本不同的分类,那么就会产生误导性的结果。
如果想要合并多个水平,那么可以使用 fct_collapse()
函数。对于每个新水平,提供一个包含原水平的向量:
gss_cat %>% mutate(partyid = fct_collapse(partyid,
other = c("No answer", "Don't know", "Other party"),
rep = c("Strong republican", "Not str republican"),
ind = c("Ind,near rep", "Independent", "Ind,near dem"),
dem = c("Not str democrat","Strong democrat")
)) %>% count(partyid)
partyid | n |
---|---|
<fct> | <int> |
other | 548 |
rep | 5346 |
ind | 8409 |
dem | 7180 |
使用 lubridate 处理日期和时间
需求不大,这里省略了。