前言
前面讲过了 R 自带的读取矩阵型数据的方法,如 read.csv,read.table 等。
下面我要介绍的是 tidyverse 中的 readr 包提供的读取矩阵型数据的方式
readr 的目标是提供一种快速友好的方式来读取矩阵型数据,如 csv, tsv 和 fwf 等。
使用
1. 导入包
library(tidyverse)
函数
readr 支持 7 种文件格式,对应于 7 个 read_ 开头的函数
-
read_csv():CSV文件
-
read_csv2(): 分号;分隔文件
-
read_tsv():tab分隔文件
-
read_delim(): 一般带分隔符的文件
-
read_fwf(): 固定宽度的文件
-
read_table(): 表格文件,列之间用空格隔开
-
read_log():web日志文件
通常只要提供文件的路径就可以读取文件,比如读取示例文件
> mtcars <- read_csv(readr_example("mtcars.csv"))
─ Column specification ────────────────────────────────────────
cols(
mpg = col_double(),
cyl = col_double(),
disp = col_double(),
hp = col_double(),
drat = col_double(),
wt = col_double(),
qsec = col_double(),
vs = col_double(),
am = col_double(),
gear = col_double(),
carb = col_double()
)
当你使用 read_csv 时,将会打印每列的列名以及类型,函数自动推断数据的类型
当然在读取文件时,你也可以为每列指定类型
mtcars <- read_csv(readr_example("mtcars.csv"), col_types =
cols(
mpg = col_double(),
cyl = col_integer(),
disp = col_double(),
hp = col_integer(),
drat = col_double(),
vs = col_integer(),
wt = col_double(),
qsec = col_double(),
am = col_integer(),
gear = col_integer(),
carb = col_integer()
)
)
你也可以提供一个内联的 CSV 文件
read_csv("a,b,c
1,2,3
4,5,6")
在这两种读取方式中,都是默认将第一行作为列名。当然,你也可以调整这种模式:
-
当你的文件前几行是数据的描绘信息,并不是你想要的,你可以使用
skip = n跳过前n行。或者,使用
comment = "#"删除所有以#开头的行
> read_csv("The first line of metadata
+ The second line of metadata
+ x,y,z
+ 1,2,3", skip = 2)
# A tibble: 1 x 3
x y z
<dbl> <dbl> <dbl>
1 1 2 3
> read_csv("# A comment I want to skip
+ x,y,z
+ 1,2,3", comment = "#")
# A tibble: 1 x 3
x y z
<dbl> <dbl> <dbl>
1 1 2 3
- 有时,你的数据并没有表头,你可以使用
col_names = FALSE告诉read_csv()不要把第一行作为表头,然后用X1到Xn依次标注
> 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,用来标识缺省的值
read_csv("a,b,c\n1,2,.", na = ".")
# A tibble: 1 x 3
a b c
<dbl> <dbl> <lgl>
1 1 2 NA
对于 read_tsv() 和 read_fwf() 的读取方式,也是类似的,就不再做详细介绍了。
如果想要进一步了解读取方式,那就要知道 readr 是如何解析每一列,并如何转换为 R 向量的。
与 R 基础操作对比
你可能会问,为什么我不直接用 R 自带的 read.csv 或其他类似的 read.table 等函数来读取文件呢?
其原因是:
-
首先,通常它的读取速度比
R基础函数快很多(大约10倍)。在长时间的运行过程中,会有一个进度条,让你知道程序运行到哪里了。如果你对速度的追求更高,可以尝试data.table::fread()函数,但是它没有readr的兼容性好 -
其次,它们产生的是
tibble格式的数据,不会将字符串转换为factor,使用行名或列名。 -
最后,它们更容易复制,基础的
R函数是从你的操作系统和环境变量继承了一些行为,因此在你的计算机上运行的代码可能无法在其他计算机上运行。
思考练习
-
使用什么函数读取
|分隔的文件? -
除了
file、skip和comment之外,read_csv()和read_tsv()还有哪些共同的参数? -
read_fwf()最重要的参数是什么? -
有时
CSV文件中的字符串包含,符号。为了避免这种问题,它们需要被一个引号括起来,比如"或'。默认情况下
read_csv()假设字符被"包裹,如何读取下面的文本?
"x,y\n1,'a,b'"
- 确定下列每个内联
CSV文件的错误所在。运行代码时会发生什么?
read_csv("a,b\n1,2,3\n4,5,6")
read_csv("a,b,c\n1,2\n1,2,3,4")
read_csv("a,b\n\"1")
read_csv("a,b\n1,2\na,b")
read_csv("a;b\n1;3")
解析向量
在深入了解 readr 如何从磁盘读取文件之前,我们需要稍微讨论一下 parse_*() 函数
这些函数接受一个字符向量并返回一个更专门的向量,如 logical, integer 或 date
> str(parse_logical(c("TRUE", "FALSE", "NA")))
logi [1:3] TRUE FALSE NA
> str(parse_integer(c("1", "2", "3")))
int [1:3] 1 2 3
> str(parse_date(c("2010-01-01", "1979-10-14")))
Date[1:2], format: "2010-01-01" "1979-10-14"
这些函数本身很有用,但也是 readr 的一个重要构建部分。
在了解完本节中各个解析器的工作原理后,我们将在下一节中回过头来看看它们是如何组合在一起解析完整的文件
和其他许多 tidyverse 函数一样,parse_*() 函数也是统一的,第一个参数是要解析的字符向量,而 na 参数指定哪些字符串应该被视为缺失值
parse_integer(c("1", "231", ".", "456"), na = ".")
[1] 1 231 NA 456
如果解析失败,你会得到一个警告
> x <- parse_integer(c("123", "345", "abc", "123.45"))
Warning: 2 parsing failures.
row col expected actual
3 -- an integer abc
4 -- no trailing characters 123.45
但是错误并不会包含在输出中
> x
[1] 123 345 NA NA
attr(,"problems")
# A tibble: 2 x 4
row col expected actual
<int> <int> <chr> <chr>
1 3 NA an integer abc
2 4 NA no trailing characters 123.45
如果有许多解析失败,你可以使用 problems() 来获得完整的信息。这会返回一个 tibble,然后您可以使用 dplyr 对其进行操作。
> problems(x)
# A tibble: 2 x 4
row col expected actual
<int> <int> <chr> <chr>
1 3 NA an integer abc
2 4 NA no trailing characters 123.45
使用解析器主要是为了理解什么是可用的,以及它们是如何处理不同类型的输入。
总共有八个特别重要的解析器
-
parse_logical()和parse_integer()分别解析逻辑和整数值 -
parse_double()是严格的数字解析器,parse_number()是灵活的数字解析器。这些并不是你想得那么容易,因为世界上不同地方的数字书写方式不同。 -
parse_character()解析字符串 -
parse_factor()创建因子 -
parse_datetime(),parse_date(), 和parse_time()可以解析各种时间和日期格式。
解析器
1. 数值
对数值的解析看起来挺简单的,但其实有几个问题需要解决:
-
世界上不同地区的人写数字的方式会有所不同,比如,一些国家使用
.来区分实数的整数部分和小数部分,而其他国家使用,来区分 -
数字在不同的使用场景会被不同的字符包裹,比如
$1000,10% -
数字通常包含分组字符,方便阅读。例如
1,000,000
为了解决第一个问题,readr 提供了 locale 参数来指定不同地区的解析方式。
解析数字时,最重要的选项是用于小数点的字符,你可以设置新的 locale 并指定 decimal_mark 参数的值来覆盖默认的 .
> parse_double("1.23")
[1] 1.23
> parse_double("1,23", locale = locale(decimal_mark = ","))
[1] 1.23
parse_number() 用于解决第二个问题:它会忽略数字前后的非数字字符,这对货币和百分比数字特别有用,但也适用于提取文本中嵌入的数字
> parse_number("$100")
[1] 100
> parse_number("20%")
[1] 20
> parse_number("It cost $123.45")
[1] 123.45
最后一个问题可以通过结合 parse_number() 和 locale 来忽略分组标记
# Used in America
> parse_number("$123,456,789")
[1] 123456789
# Used in many parts of Europe
> parse_number("123.456.789", locale = locale(grouping_mark = "."))
[1] 123456789
# Used in Switzerland
> parse_number("123'456'789", locale = locale(grouping_mark = "'"))
[1] 123456789
2. 字符串
用 parse_character() 来解析字符串看起来应该是很简单的,能够直接把输入返回。
但是,生活并不是那么简单的,因为同一个字符串的表示方法有很多种。
为了理解解析时发生了,我们需要深入了解计算机如何来表示字符串的。在 R 中,我们可以使用 charToRaw() 获得字符串的底层表示形式
> charToRaw("Hadley")
[1] 48 61 64 6c 65 79
每个十六进制数代表一个字节的信息,48 表示 H, 61 表示 a 等等。从十六进制数到字符的映射称为编码,在这种情况下编码称为 ASCII。
ASCII 在表示英语字符方面做得很好,因为它是美国信息交换的标准代码
而对于非英文来说,事情就变得更加复杂了点。在计算机早期时代,有许多编码非英语字符的竞争标准,要正确解释一个字符串,您需要同时知道值和编码
例如,两种常见的编码 Latin1(ISO-8859-1,用于西欧语言)和 Latin2(ISO-8859-2,用于东欧语言)。在 Latin1 中,字节 b1 表示 ±,但是在 Latin2 中表示的是 ą。
幸运的是,现在有一个标准几乎在所有地方都能够得到支持:UTF-8。UTF-8 可以编码当今人类使用的几乎所有字符,以及许多额外的符号(比如 emoji)
readr 使用的都是 UTF-8,它假设您的数据在读取时是 UTF-8 编码的,并且在写入时也总是使用它。
如果你的系统不支持 UTF-8 格式(你的机子不会老到这种程度的),那它看起来可能非常奇怪,已经发生了乱码。
看个例子
> x1 <- "El Ni\xf1o was particularly bad this year"
> x2 <- "\x82\xb1\x82\xf1\x82\xc9\x82\xbf\x82\xcd"
> x1
[1] "El Ni\xf1o was particularly bad this year"
> x2
[1] "\x82\xb1\x82\xf1\x82\u0242\xbf\x82\xcd"
非英文字符并未被正确转译
使用 parse_character() 时指定编码格式
parse_character(x1, locale = locale(encoding = "Latin1"))
#> [1] "El Niño was particularly bad this year"
parse_character(x2, locale = locale(encoding = "Shift-JIS"))
#> [1] "こんにちは"
那如果我们不知道字符串的编码方式,要如何解码出正确的字符呢?
如果幸运的话,它会在数据文档中的某个地方。不幸的是,这种情况很少发生,因此 readr 提供了 guess_encoding() 来帮助您解决这个问题
它也并不是万无一失的,当你有大量的文本时效果会更好,但它是一个合理的起点。在找到合适的编码之前,可以尝试几种不同的编码。
> guess_encoding(charToRaw(x1))
# A tibble: 2 x 2
encoding confidence
<chr> <dbl>
1 ISO-8859-1 0.46
2 ISO-8859-9 0.23
> guess_encoding(charToRaw(x2))
# A tibble: 1 x 2
encoding confidence
<chr> <dbl>
1 KOI8-R 0.42
guess_encoding() 的第一个参数可以是文件的路径,也可以是字符串的原始向量
3. 因子
R 使用因子来表示一组所有可能值已知的分类变量。
通过向 parse_factor() 的 levels 参数传入一个已知的向量作为分类向量,当出现意外的值时会生成警告信息
> fruit <- c("apple", "banana")
> parse_factor(c("apple", "banana", "bananana"), levels = fruit)
Warning: 1 parsing failure.
row col expected actual
3 -- value in level set bananana
[1] apple banana <NA>
attr(,"problems")
# A tibble: 1 x 4
row col expected actual
<int> <int> <chr> <chr>
1 3 NA value in level set bananana
Levels: apple banana
4. 时间和日期
你可以根据需要从三个解析器中选择相应的解析器
parse_datetime():ISO8601格式的日期和时间,ISO8601是一个国际标准,其中日期的组成部分按从大到小的顺序排列:年、月、日、时、分、秒
[1] "2010-10-01 20:10:00 UTC"
> parse_datetime("20101010")
[1] "2010-10-10 UTC"
parse_date(): 接受4位数的年份yyyy-mm-dd或yyyy/mm/dd
> parse_date("2010-10-01")
[1] "2010-10-01"
parse_time(): 接受格式hh:mm(:ss am/pm)
> parse_time("01:10 am")
01:10:00
> parse_time("20:10:01")
20:10:01
如果这些默认值不适用于您的数据,您可以提供自己的日期时间格式,由以下几部分组成
Year
%Y:4位数字%y:2位数字;00-69 -> 2000-2069,70-99 -> 1970-1999
Month
%m:2位数字%b: 简写, 如Jan%B: 全称, 如January
Day
%d:2位数字%e: 可选的前导空格
Time
%H:0-23小时%I:0-12小时, 必须包含%p%p:AM/PM%M: 分钟%S: 整数秒数%OS: 实数秒数%Z: 时区%z: 相对于UTC的偏移, 如+0800
Non-digits
%.: 跳过一个非数字字符%*: 跳过任意个非数字字符
示例
> parse_date("01/02/15", "%m/%d/%y")
[1] "2015-01-02"
> parse_date("01/02/15", "%d/%m/%y")
[1] "2015-02-01"
> parse_date("01/02/15", "%y/%m/%d")
[1] "2001-02-15"
如果 %B 或 %b 使用的是非英语的月份名,你需要用 locale() 设置语言。
详情参见 date_names_langs() 中的内置语言列表,或者如果没有包含你的语言,可以用 date_names() 创建你自己的语言
> parse_date("1 janvier 2015", "%d %B %Y", locale = locale("fr"))
[1] "2015-01-01"
下面是上节的思考练习。是一位读者写的,我发出来给大家参考参考
http://note.youdao.com/s/Ls1QE1oN
解析文件
讲到这里,你应该已经很清楚怎么解析单个向量了。那么让我们回到上节引出的 readr 是如何解析文件的问题。在本节中,将介绍另外两个新的问题
-
readr如何自动猜测每列的类型。 -
如何覆盖默认规范。
1. 策略
readr 使用启发式方法计算出每一列的类型:它读取前 1000 行,并使用一些(适度保守的)启发式方法来找出每一列的类型。
你可以使用 guess_parser() 和 parse_guess() 解析字符向量来模拟这个过程,guess_parser() 返回 readr 的最佳猜测,parse_guess() 使用该猜测来解析列
> guess_parser("2010-10-01")
[1] "date"
> guess_parser("15:01")
[1] "time"
> guess_parser(c("TRUE", "FALSE"))
[1] "logical"
> guess_parser(c("1", "5", "9"))
[1] "double"
> guess_parser(c("12,352,561"))
[1] "number"
>
> str(parse_guess("2010-10-10"))
Date[1:1], format: "2010-10-10"
启发式方法尝试以下每种类型,在找到匹配时停止:
logical: 只包含F,T,FALSE, 或TRUEinteger: 只包含数字字符和 -double: 只包含有效双精度数字(包含科学计数法4.5e-5)number: 包含有效的双精度数据,其中包含分组标记time: 匹配默认的time_formatdate: 匹配默认的date_formatdate-time: 任何ISO8601格式
如果这些规则都不适用,那么列将保持为字符串向量。
2. Problems
这些默认值并不总是适用于较大的文件。存在两个基本问题:
-
前
1000行可能是一个特殊情况,readr猜测是一种不够通用的类型。例如,您可能有一个双精度列,它的前1000行中只包含整数 -
列可能包含许多缺少的值。如果前
1000行只包含NA,readr会猜测它是一个逻辑向量,而您可能希望将其解析为更具体的内容。
> challenge <- read_csv(readr_example("challenge.csv"))
─ Column specification ────────────────────────────────────────
cols(
x = col_double(),
y = col_logical()
)
Warning: 1000 parsing failures.
row col expected actual file
1001 y 1/0/T/F/TRUE/FALSE 2015-01-16 '/Library/Frameworks/R.framework/Versions/3.6/Resources/library/readr/extdata/challenge.csv'
1002 y 1/0/T/F/TRUE/FALSE 2018-05-18 '/Library/Frameworks/R.framework/Versions/3.6/Resources/library/readr/extdata/challenge.csv'
1003 y 1/0/T/F/TRUE/FALSE 2015-09-05 '/Library/Frameworks/R.framework/Versions/3.6/Resources/library/readr/extdata/challenge.csv'
1004 y 1/0/T/F/TRUE/FALSE 2012-11-28 '/Library/Frameworks/R.framework/Versions/3.6/Resources/library/readr/extdata/challenge.csv'
1005 y 1/0/T/F/TRUE/FALSE 2020-01-13 '/Library/Frameworks/R.framework/Versions/3.6/Resources/library/readr/extdata/challenge.csv'
.... ... .................. .......... ............................................................................................
See problems(...) for more details.
readr_example() 会寻找包自带的数据文件
可以看到打印了两部分的输出:第一部分是每列的解析格式,分别为 double 和 logical。第二部分,显示了 1000 行之后发生了解析错误,并打印了前 5 个解析错误信息
我们可以使用 problems() 显示更加详细的信息
> problems(challenge)
# A tibble: 1,000 x 5
row col expected actual file
<int> <chr> <chr> <chr> <chr>
1 1001 y 1/0/T/F/TRUE/F… 2015-01-… '/Library/Frameworks/R.framework/Versions/3.6/Resources/libra…
2 1002 y 1/0/T/F/TRUE/F… 2018-05-… '/Library/Frameworks/R.framework/Versions/3.6/Resources/libra…
3 1003 y 1/0/T/F/TRUE/F… 2015-09-… '/Library/Frameworks/R.framework/Versions/3.6/Resources/libra…
4 1004 y 1/0/T/F/TRUE/F… 2012-11-… '/Library/Frameworks/R.framework/Versions/3.6/Resources/libra…
5 1005 y 1/0/T/F/TRUE/F… 2020-01-… '/Library/Frameworks/R.framework/Versions/3.6/Resources/libra…
6 1006 y 1/0/T/F/TRUE/F… 2016-04-… '/Library/Frameworks/R.framework/Versions/3.6/Resources/libra…
# … with 994 more rows
一个较好的策略是一列一列地测试,直到没有问题为止。
在这里我们可以看到 y 列有许多解析问题,我们看下后面几行
0.1635163405444473,2018-03-29
0.47193897631950676,2014-08-04
0.7183186465408653,2015-08-16
0.26987858884967864,2020-02-04
0.608237189007923,2019-01-06
解析后变成了
> tail(challenge)
# A tibble: 6 x 2
x y
<dbl> <lgl>
1 0.805 NA
2 0.164 NA
3 0.472 NA
4 0.718 NA
5 0.270 NA
6 0.608 NA
这表明我们需要使用一个日期解析器。要修复调用,我们首先将列格式输出复制并粘贴到原始调用。
challenge <- read_csv(
readr_example("challenge.csv"),
col_types = cols(
x = col_double(),
y = col_date() # col_logical()
)
)
然后,将 col_logical() 改为 col_date()
> tail(challenge)
# A tibble: 6 x 2
x y
<dbl> <date>
1 0.805 2019-11-21
2 0.164 2018-03-29
3 0.472 2014-08-04
4 0.718 2015-08-16
5 0.270 2020-02-04
6 0.608 2019-01-06
每个 parse_*() 函数都有一个对应的 col_*() 函数。可以使用 parse_*() 解析 R 中的字符串向量,当你要告诉 readr 如何解析数据时,可以使用 col_*() 函数
强烈推荐使用 col_types 参数指定解析方式,这可以确保您拥有一个一致且可重复的数据导入脚本。
如果您依赖于默认猜测策略,但是您的数据发生了更改,readr 会继续读入它。如果您想严格控制数据格式,请使用 stop_ for_problems(): 如果有任何解析问题,它将抛出错误并停止脚本
3. 其他策略
还有其他一些通用策略可以帮助您解析文件:
- 在前面的例子中,如果我们比默认多看一行,我们可以一次正确解析
> challenge2 <- read_csv(readr_example("challenge.csv"), guess_max = 1001)
─ Column specification ────────────────────────────────────────
cols(
x = col_double(),
y = col_date(format = "")
)
- 有时,如果你只是把所有的列作为字符向量来读取,就会更容易诊断问题
challenge2 <- read_csv(readr_example("challenge.csv"),
col_types = cols(.default = col_character())
)
这在与 type_convert() 结合使用时非常有用,它将启发式解析应用于数据中的字符列
> df <- tribble(
+ ~x, ~y,
+ "1", "1.21",
+ "2", "2.32",
+ "3", "4.56"
+ )
> df
# A tibble: 3 x 2
x y
<chr> <chr>
1 1 1.21
2 2 2.32
3 3 4.56
> type_convert(df)
─ Column specification ────────────────────────────────────────
cols(
x = col_double(),
y = col_double()
)
# A tibble: 3 x 2
x y
<dbl> <dbl>
1 1 1.21
2 2 2.32
3 3 4.56
-
如果您正在读取一个非常大的文件,您可能希望将
n_max设置为一个较小的数字,如10,000或100,000。这将消除常见问题的同时加速您的迭代 -
如果您遇到了解析问题,有时使用
read_lines读入一行的字符向量会更容易。甚至用read_file读取长度为1的字符向量
写入文件
readr 还提供了两个函数用于将输出写入到文件中: write_csv() 和 write_tsv()。这两个函数都:
- 总是用
UTF-8编码字符串 - 以
ISO8601格式保存日期和日期时间
如果想要保存为 Excel 格式的 csv 文件,可以使用 write_excel_csv()。
最重要的两个参数是
x: 需要保存的数据path: 数据保存路径
你还可以指定如何使用 na 代替缺失值,以及是否想要追加到现有文件
write_csv(challenge, "challenge.csv")
注意: 在保存到 csv 时,类型信息会丢失
> challenge
# A tibble: 2,000 x 2
x y
<dbl> <date>
1 404 NA
2 4172 NA
3 3004 NA
4 787 NA
5 37 NA
6 2332 NA
# … with 1,994 more rows
> write_csv(challenge, "challenge-2.csv")
> read_csv("challenge-2.csv")
─ Column specification ────────────────────────────────────────
cols(
x = col_double(),
y = col_logical()
)
Warning: 1000 parsing failures.
row col expected actual file
1001 y 1/0/T/F/TRUE/FALSE 2015-01-16 'challenge-2.csv'
1002 y 1/0/T/F/TRUE/FALSE 2018-05-18 'challenge-2.csv'
1003 y 1/0/T/F/TRUE/FALSE 2015-09-05 'challenge-2.csv'
1004 y 1/0/T/F/TRUE/FALSE 2012-11-28 'challenge-2.csv'
1005 y 1/0/T/F/TRUE/FALSE 2020-01-13 'challenge-2.csv'
.... ... .................. .......... .................
See problems(...) for more details.
# A tibble: 2,000 x 2
x y
<dbl> <lgl>
1 404 NA
2 4172 NA
3 3004 NA
4 787 NA
5 37 NA
6 2332 NA
# … with 1,994 more rows
这使得 csv 在缓存中间结果时有点不可靠。可以使用其他两种方法
write_rds()和read_rds()是对基本函数readRDS()和saveRDS()的统一包装。将数据存储为R的自定义二进制格式称为RDS
> write_rds(challenge, "challenge.rds")
> read_rds("challenge.rds")
# A tibble: 2,000 x 2
x y
<dbl> <date>
1 404 NA
2 4172 NA
3 3004 NA
4 787 NA
5 37 NA
6 2332 NA
7 2489 NA
8 1449 NA
9 3665 NA
10 3863 NA
# … with 1,990 more rows
feather包实现了一种可以跨编程语言共享的快速二进制文件格式
> library(feather)
> write_feather(challenge, "challenge.feather")
> read_feather("challenge.feather")
# A tibble: 2,000 x 2
x y
<dbl> <date>
1 404 NA
2 4172 NA
3 3004 NA
4 787 NA
5 37 NA
6 2332 NA
7 2489 NA
8 1449 NA
9 3665 NA
10 3863 NA
# … with 1,990 more rows
feather 比 RDS 更快,且可以脱离 R 使用。
3390

被折叠的 条评论
为什么被折叠?



