R语言数据导入

https://wp.me/p80aHo-2uJ

Table of Contents

引言

使用 R 包提供的数据进行练习,是学习数据科学工具的好方法,但你总会在某个时候想把所学应用到自己的数据上。在本章中,你将学习将数据文件读取到 R 的基础知识。

具体来说,重点介绍读取纯文本矩形文件。我们将从处理列名、类型和缺失数据等特性的实用建议开始。然后,你将学习如何一次从多个文件读取数据,以及如何将 R 中的数据写入文件。最后,你将学习如何在 R 中手工创建数据框。

从文件读取数据

首先,我们将重点介绍最常见的矩形数据文件类型:CSV,即逗号分隔值(comma-separated values)的缩写。下面是一个简单的 CSV 文件示例。第一行通常称为表头行,提供列名,接下来的六行提供数据。各列之间用逗号分隔,也就是用逗号作为分隔符。

Student ID,Full Name,favourite.food,mealPlan,AGE
1,Sunil Huffmann,Strawberry yoghurt,Lunch only,4
2,Barclay Lynn,French fries,Lunch only,5
3,Jayendra Lyne,N/A,Breakfast and lunch,7
4,Leon Rossini,Anchovies,Lunch only,
5,Chidiegwu Dunkel,Pizza,Breakfast and lunch,five
6,Güvenç Attila,Ice cream,Lunch only,6
Student IDFull Namefavourite.foodmealPlanAGE
1Sunil HuffmannStrawberry yoghurtLunch only4
2Barclay LynnFrench friesLunch only5
3Jayendra LyneN/ABreakfast and lunch7
4Leon RossiniAnchoviesLunch onlyNA
5Chidiegwu DunkelPizzaBreakfast and lunchfive
6Güvenç AttilaIce creamLunch only6

我们可以使用 read_csv() 将这个文件读入 R。第一个参数最重要:它是文件路径。你可以把路径理解为文件的地址:这个文件名为 students.csv,位于 data 文件夹中。

students <- read_csv("data/students.csv")
#> Rows: 6 Columns: 5
#> ── Column specification ─────────────────────────────────────────────────────
#> Delimiter: ","
#> chr (4): Full Name, favourite.food, mealPlan, AGE
#> dbl (1): Student ID
#>
#> ℹ 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.

如果你的项目中有一个 data 文件夹,并且其中包含 students.csv 文件,那么上面的代码就可以正常运行。你可以从 https://pos.it/r4ds-students-csv 下载 students.csv 文件,或者直接使用以下方式从该 URL 读取它:

library(readr)
students <- read_csv("https://pos.it/r4ds-students-csv")

当你运行 read_csv() 时,它会打印一条消息,告诉你数据的行数和列数、所使用的分隔符,以及列规范(按列中所包含的数据类型组织的列名)。它还会打印一些关于如何获取完整列规范的信息,以及如何静默这条消息的信息。

实用建议

当你读入数据后,第一步通常是以某种方式对其进行转换,使其在后续分析中更容易处理。让我们带着这一点,再来看看学生数据。

students
#> # A tibble: 6 × 5
#> `Student ID` `Full Name` favourite.food mealPlan AGE
#> <dbl> <chr> <chr> <chr> <chr>
#> 1 1 Sunil Huffmann Strawberry yoghurt Lunch only 4
#> 2 2 Barclay Lynn French fries Lunch only 5
#> 3 3 Jayendra Lyne N/A Breakfast and lunch 7
#> 4 4 Leon Rossini Anchovies Lunch only <NA>
#> 5 5 Chidiegwu Dunkel Pizza Breakfast and lunch five
#> 6 6 Güvenç Attila Ice cream Lunch only 6

在 favourite.food 这一列中,有一堆食物项,之后还有字符字符串 N/A,它本应是一个真正的 NA,R 会将其识别为“不可用”。我们可以使用 na 参数来解决这个问题。默认情况下,read_csv() 只会将该数据集中的空字符串(””)识别为 NA,而我们希望它也能识别字符字符串 “N/A”。

students <- read_csv("https://pos.it/r4ds-students-csv", na = c("N/A", ""))

你可能还会注意到,Student ID 和 Full Name 两列被反引号包围着。这是因为它们包含空格,打破了 R 对变量名的通常规则;它们属于非语法名称。要引用这些变量,你需要用反引号将它们括起来,`:

students |>
rename(
student_id = `Student ID`,
full_name = `Full Name`
)
#> # A tibble: 6 × 5
#> student_id full_name favourite.food mealPlan AGE
#> <dbl> <chr> <chr> <chr> <chr>
#> 1 1 Sunil Huffmann Strawberry yoghurt Lunch only 4
#> 2 2 Barclay Lynn French fries Lunch only 5
#> 3 3 Jayendra Lyne <NA> Breakfast and lunch 7
#> 4 4 Leon Rossini Anchovies Lunch only <NA>
#> 5 5 Chidiegwu Dunkel Pizza Breakfast and lunch five
#> 6 6 Güvenç Attila Ice cream Lunch only 6

另一种方法是使用 janitor::clean_names(),借助一些启发式规则一次性把它们全部转换为 snake case。

library(janitor)
students |> janitor::clean_names()
#> # A tibble: 6 × 5
#> student_id full_name favourite_food meal_plan age
#> <dbl> <chr> <chr> <chr> <chr>
#> 1 1 Sunil Huffmann Strawberry yoghurt Lunch only 4
#> 2 2 Barclay Lynn French fries Lunch only 5
#> 3 3 Jayendra Lyne <NA> Breakfast and lunch 7
#> 4 4 Leon Rossini Anchovies Lunch only <NA>
#> 5 5 Chidiegwu Dunkel Pizza Breakfast and lunch five
#> 6 6 Güvenç Attila Ice cream Lunch only 6

在读入数据之后,另一个常见任务是考虑变量类型。例如,meal_plan 是一个分类变量,它有一组已知的可能取值,在 R 中应将其表示为因子:

library(dplyr)
students |>
janitor::clean_names() |>
mutate(meal_plan = factor(meal_plan))
#> # A tibble: 6 × 5
#> student_id full_name favourite_food meal_plan age
#> <dbl> <chr> <chr> <fct> <chr>
#> 1 1 Sunil Huffmann Strawberry yoghurt Lunch only 4
#> 2 2 Barclay Lynn French fries Lunch only 5
#> 3 3 Jayendra Lyne <NA> Breakfast and lunch 7
#> 4 4 Leon Rossini Anchovies Lunch only <NA>
#> 5 5 Chidiegwu Dunkel Pizza Breakfast and lunch five
#> 6 6 Güvenç Attila Ice cream Lunch only 6

请注意,meal_plan 变量中的值保持不变,但变量名下方标示的变量类型已经从字符型(<chr>)变为因子型(<fct>)。

在分析这些数据之前,你可能需要先修正 age 列。目前,age 是一个字符型变量,因为其中一个观测值被写成了 five,而不是数字 5。

students <- students |>
janitor::clean_names() |>
mutate(
meal_plan = factor(meal_plan),
age = parse_number(if_else(age == "five", "5", age))
)
students
#> # A tibble: 6 × 5
#> student_id full_name favourite_food meal_plan age
#> <dbl> <chr> <chr> <fct> <dbl>
#> 1 1 Sunil Huffmann Strawberry yoghurt Lunch only 4
#> 2 2 Barclay Lynn French fries Lunch only 5
#> 3 3 Jayendra Lyne <NA> Breakfast and lunch 7
#> 4 4 Leon Rossini Anchovies Lunch only NA
#> 5 5 Chidiegwu Dunkel Pizza Breakfast and lunch 5
#> 6 6 Güvenç Attila Ice cream Lunch only 6

这里一个新的函数是 if_else(),它有三个参数。第一个参数 test 应该是一个逻辑向量。当 test 为 TRUE 时,结果将包含第二个参数 yes 的值;当它为 FALSE 时,结果将包含第三个参数 no 的值。这里我们是在说,如果 age 是字符字符串 "five",就把它改成 "5";如果不是,就保持为 age

其他参数

还有另外几个重要参数需要提及,而如果我们先向你展示一个实用技巧,它们会更容易说明:read_csv() 可以读取你创建并格式化成 CSV 文件样式的文本字符串:

read_csv(
"a,b,c
1,2,3
4,5,6"
)

通常,read_csv() 会使用数据的第一行作为列名,这是一种非常常见的约定。但文件顶部包含几行元数据也并不少见。你可以使用 skip = n 来跳过前 n 行,或者使用 comment = "#" 来删除所有以(例如)# 开头的行:

read_csv(
"The first line of metadata
The second line of metadata
x,y,z
1,2,3",
skip = 2
)
read_csv(
"# A comment I want to skip
x,y,z
1,2,3",
comment = "#"
)

在其他情况下,数据可能没有列名。你可以使用 col_names = FALSE 告诉 read_csv() 不要将第一行当作标题,而是按顺序将它们标记为 X1 到 Xn

read_csv(
"1,2,3
4,5,6",
col_names = FALSE
)

或者,你也可以向 col_names 传入一个字符向量,它将被用作列名:

read_csv(
"1,2,3
4,5,6",
col_names = c("x", "y", "z")
)

这些参数已经足够让你读取在实践中遇到的大多数 CSV 文件。(至于其余的情况,你需要仔细检查你的 .csv 文件,并阅读 read_csv() 其他许多参数的文档。)

其他文件类型

一旦你掌握了 read_csv(),使用 readr 的其他函数就很简单了;关键只是知道该使用哪个函数:

read_csv2() 读取以分号分隔的文件。这类文件使用 ; 而不是 , 来分隔字段,并且在以 , 作为小数点标记的国家很常见。

read_tsv() 读取以制表符分隔的文件。

read_delim() 读取使用任意分隔符的文件;如果你没有指定分隔符,它会尝试自动猜测。

read_fwf() 读取固定宽度文件。你可以用 fwf_widths() 按字段宽度指定,或用 fwf_positions() 按字段位置指定。

read_table() 读取固定宽度文件的一种常见变体,其中各列由空白分隔。

read_log() 读取 Apache 风格的日志文件。

练习

  • 你会使用什么函数来读取一个字段由“|”分隔的文件?
read_delim()
  • 有时 CSV 文件中的字符串包含逗号。为防止它们引发问题,需要用引号字符将其括起来,例如 " 或 '。默认情况下,read_csv() 假定引号字符是 "。要将下面的文本读入数据框,你需要为 read_csv() 指定什么参数?”x,y\n1,’a,b’”
read_csv(
"x,y\n1,'a,b'",
quote = "'"
)

控制列类型

CSV 文件不包含任何关于每个变量类型的信息(即它是逻辑型、数值型、字符串型等),因此 readr 会尝试猜测其类型。本节将描述这种猜测过程是如何工作的,如何解决一些导致其失败的常见问题,以及在需要时如何自行提供列类型。最后,我们还会提到一些通用策略;当 readr 失败得很严重、你需要更深入了解文件结构时,这些策略会很有帮助。

猜测类型

readr 使用一种启发式方法来判断列类型。对于每一列,它会从第一行到最后一行等间隔抽取 1,000 行的值,并忽略缺失值。然后它会依次回答以下问题:

  • 它是否只包含 F、T、FALSE 或 TRUE(忽略大小写)?如果是,那么它是逻辑型。
  • 它是否只包含数字(例如 1、-4.5、5e6、Inf)?如果是,那么它是数值型。
  • 它是否符合 ISO8601 标准?如果是,那么它是日期或日期时间。

否则,它一定是字符串。
你可以在下面这个简单示例中看到这种行为:

read_csv("
logical,numeric,date,string
TRUE,1,2021-01-15,abc
false,4.5,2021-02-15,def
T,Inf,2021-02-16,ghi
")

如果你有一个干净的数据集,这种启发式方法效果很好;但在现实中,你会遇到各种奇怪而又“美丽”的失败情况。

缺失值、列类型和问题

列检测最常见的失败方式是:某一列包含了意料之外的值,结果你得到的是字符型列,而不是更具体的类型。造成这种情况最常见的原因之一,是缺失值使用了 readr 预期之外的其他标记方式来记录。

以这个简单的单列表 CSV 文件为例:

simple_csv <- "
x
10
.
20
30"

如果我们不添加任何额外参数来读取它,x 会变成字符列:

read_csv(simple_csv)

在这个非常小的例子里,你很容易就能看出缺失值“.”。但如果你有成千上万行数据,其中只有少数缺失值以散布其中的“.”来表示,又会怎样呢?一种做法是告诉 readr,x 是数值型列,然后看看它在哪里失败。你可以使用 col_types 参数来做到这一点,它接受一个命名列表,其中名称与 CSV 文件中的列名相匹配:

df <- read_csv(
simple_csv,
col_types = list(x = col_double())
)
df

现在 read_csv() 会报告存在一个问题,并告诉我们可以通过 problems() 了解更多信息:

这告诉我们,在第 3 行、第 1 列出现了一个问题:readr 期望得到一个双精度数,但实际得到了一个“.”。这说明该数据集使用“.”表示缺失值。于是我们设置 na = ".",自动推断就成功了,得到了我们想要的数值列:

read_csv(simple_csv, na = ".")

列类型

readr 为你提供了总共九种可用的列类型:

  • col_logical() 和 col_double() 用于读取逻辑值和实数。它们相对很少需要手动指定(如上所示的情况除外),因为 readr 通常会自动为你推断出来。
  • col_integer() 用于读取整数。我们很少区分整数和双精度数,因为它们在功能上等价,但显式读取整数有时会很有用,因为它们占用的内存只有双精度数的一半。
  • col_character() 用于读取字符串。当某一列是数值型标识符时,显式指定它会很有用,也就是那种由一长串数字组成、用于标识某个对象,但对其进行数学运算并没有意义的数据。例如电话号码、社会保险号、信用卡号码等。
  • col_factor()col_date() 和 col_datetime() 分别用于创建因子、日期和日期时间。
  • col_number() 是一种更宽松的数值解析器,它会忽略非数值部分,尤其适合处理货币。你会在第 13 章进一步了解它。
  • col_skip() 会跳过某一列,因此不会将其包含在结果中;如果你有一个很大的 CSV 文件,但只想使用其中部分列,这样可以加快读取速度。

你也可以通过将 list() 改为 cols() 并指定 .default,来覆盖默认的启发式类型推断:

another_csv <- "
x,y,z
1,2,3"
read_csv(
another_csv,
col_types = cols(.default = col_character())
)

另一个有用的辅助函数是 cols_only(),它只会读取你指定的列:

read_csv(
another_csv,
col_types = cols_only(x = col_character())
)

从多个文件读取数据

有时你的数据会分散在多个文件中,而不是包含在一个文件里。例如,你可能有按月划分的销售数据,每个月的数据都放在单独的文件中:01-sales.csv 表示一月,02-sales.csv 表示二月,03-sales.csv 表示三月。使用 read_csv(),你可以一次性读取这些数据,并将它们按顺序叠加到同一个数据框中。

sales_files <- c("data/01-sales.csv", "data/02-sales.csv", "data/03-sales.csv")
read_csv(sales_files, id = "file")

如果你的 CSV 文件位于项目中的 data 文件夹里,上面的代码同样可以正常工作。你可以从 https://pos.it/r4ds-01-sales、https://pos.it/r4ds-02-sales 和 https://pos.it/r4ds-03-sales 下载这些文件,或者直接用下面的方法读取它们:

sales_files <- c(
"https://pos.it/r4ds-01-sales",
"https://pos.it/r4ds-02-sales",
"https://pos.it/r4ds-03-sales"
)
read_csv(sales_files, id = "file")

id 参数会在结果数据框中新增一列名为 file,用于标识数据来自哪个文件。当你读取的文件没有可用于追踪观测值原始来源的标识列时,这一点尤其有用。

如果你有很多文件需要读取,把它们的文件名逐个写成列表会很麻烦。相反,你可以使用基础函数 list.files(),通过匹配文件名中的模式来帮你找到这些文件。

sales_files <- list.files("data", pattern = "sales\\.csv$", full.names = TRUE)
sales_files
#> [1] "data/01-sales.csv" "data/02-sales.csv" "data/03-sales.csv"

Writing to a file

readr 还提供了两个用于将数据写回磁盘的实用函数:write_csv() 和 write_tsv()。这些函数最重要的参数是 x(要保存的数据框)和 file(保存位置)。你还可以通过 na 指定缺失值的写入方式,并且可以选择是否追加到已有文件中。

write_csv(students, "students.csv")

现在让我们把那个 CSV 文件再读回来。请注意,你刚刚设置的变量类型信息在保存为 CSV 时会丢失,因为你又是从一个纯文本文件重新开始读取的:

students
#> # A tibble: 6 × 5
#> student_id full_name favourite_food meal_plan age
#> <dbl> <chr> <chr> <fct> <dbl>
#> 1 1 Sunil Huffmann Strawberry yoghurt Lunch only 4
#> 2 2 Barclay Lynn French fries Lunch only 5
#> 3 3 Jayendra Lyne <NA> Breakfast and lunch 7
#> 4 4 Leon Rossini Anchovies Lunch only NA
#> 5 5 Chidiegwu Dunkel Pizza Breakfast and lunch 5
#> 6 6 Güvenç Attila Ice cream Lunch only 6
write_csv(students, "students-2.csv")
read_csv("students-2.csv")
#> # A tibble: 6 × 5
#> student_id full_name favourite_food meal_plan age
#> <dbl> <chr> <chr> <chr> <dbl>
#> 1 1 Sunil Huffmann Strawberry yoghurt Lunch only 4
#> 2 2 Barclay Lynn French fries Lunch only 5
#> 3 3 Jayendra Lyne <NA> Breakfast and lunch 7
#> 4 4 Leon Rossini Anchovies Lunch only NA
#> 5 5 Chidiegwu Dunkel Pizza Breakfast and lunch 5
#> 6 6 Güvenç Attila Ice cream Lunch only 6

这会使 CSV 在缓存中间结果时有些不太可靠——你每次加载时都需要重新创建列规格。有两个主要的替代方案:

write_rds() 和 read_rds() 是对基础函数 readRDS() 和 saveRDS() 的统一封装。它们将数据存储为 R 的自定义二进制格式,称为 RDS。这意味着当你重新加载对象时,加载的是你存储时那个完全相同的 R 对象。

write_rds(students, "students.rds")
read_rds("students.rds")
#> # A tibble: 6 × 5
#> student_id full_name favourite_food meal_plan age
#> <dbl> <chr> <chr> <fct> <dbl>
#> 1 1 Sunil Huffmann Strawberry yoghurt Lunch only 4
#> 2 2 Barclay Lynn French fries Lunch only 5
#> 3 3 Jayendra Lyne <NA> Breakfast and lunch 7
#> 4 4 Leon Rossini Anchovies Lunch only NA
#> 5 5 Chidiegwu Dunkel Pizza Breakfast and lunch 5
#> 6 6 Güvenç Attila Ice cream Lunch only 6

arrow 包允许你读写 Parquet 文件,这是一种可以跨编程语言共享的快速二进制文件格式。

library(arrow)
write_parquet(students, "students.parquet")
read_parquet("students.parquet")
#> # A tibble: 6 × 5
#> student_id full_name favourite_food meal_plan age
#> <dbl> <chr> <chr> <fct> <dbl>
#> 1 1 Sunil Huffmann Strawberry yoghurt Lunch only 4
#> 2 2 Barclay Lynn French fries Lunch only 5
#> 3 3 Jayendra Lyne NA Breakfast and lunch 7
#> 4 4 Leon Rossini Anchovies Lunch only NA
#> 5 5 Chidiegwu Dunkel Pizza Breakfast and lunch 5
#> 6 6 Güvenç Attila Ice cream Lunch only 6

Parquet 通常比 RDS 快得多,而且可以在 R 之外使用,但它需要 arrow 包。

数据输入

有时候,你需要在 R 脚本里通过少量手工录入来“手动”组装一个 tibble。这里有两个很有用的函数可以帮助你完成这件事,它们的区别在于你是按列还是按行来构建 tibble。tibble() 是按列工作的:

tibble(
x = c(1, 2, 5),
y = c("h", "m", "g"),
z = c(0.08, 0.83, 0.60)
)
#> # A tibble: 3 × 3
#> x y z
#> <dbl> <chr> <dbl>
#> 1 1 h 0.08
#> 2 2 m 0.83
#> 3 5 g 0.6

按列排列数据会让人难以看出各行之间的关系,因此可以使用 tribble(),即转置 tibble 的简称,它允许你按行排列数据。tribble() 是专为在代码中输入数据而设计的:列名以 ~ 开头,条目之间用逗号分隔。这使得少量数据可以以一种易读的形式排布出来:

tribble(
~x, ~y, ~z,
1, "h", 0.08,
2, "m", 0.83,
5, "g", 0.60
)
#> # A tibble: 3 × 3
#> x y z
#> <dbl> <chr> <dbl>
#> 1 1 h 0.08
#> 2 2 m 0.83
#> 3 5 g 0.6

总结

你已经学习了如何使用 read_csv() 加载 CSV 文件,以及如何使用 tibble() 和 tribble() 自己输入数据。你已经了解了 CSV 文件的工作方式、可能遇到的一些问题,以及如何克服这些问题。

评论

发表评论

了解 数据控|突破是我们的每一步 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读