Table of Contents
引言
使用 R 包提供的数据进行练习,是学习数据科学工具的好方法,但你总会在某个时候想把所学应用到自己的数据上。在本章中,你将学习将数据文件读取到 R 的基础知识。
具体来说,重点介绍读取纯文本矩形文件。我们将从处理列名、类型和缺失数据等特性的实用建议开始。然后,你将学习如何一次从多个文件读取数据,以及如何将 R 中的数据写入文件。最后,你将学习如何在 R 中手工创建数据框。
从文件读取数据
首先,我们将重点介绍最常见的矩形数据文件类型:CSV,即逗号分隔值(comma-separated values)的缩写。下面是一个简单的 CSV 文件示例。第一行通常称为表头行,提供列名,接下来的六行提供数据。各列之间用逗号分隔,也就是用逗号作为分隔符。
Student ID,Full Name,favourite.food,mealPlan,AGE1,Sunil Huffmann,Strawberry yoghurt,Lunch only,42,Barclay Lynn,French fries,Lunch only,53,Jayendra Lyne,N/A,Breakfast and lunch,74,Leon Rossini,Anchovies,Lunch only,5,Chidiegwu Dunkel,Pizza,Breakfast and lunch,five6,Güvenç Attila,Ice cream,Lunch only,6| 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 | NA |
| 5 | Chidiegwu Dunkel | Pizza | Breakfast and lunch | five |
| 6 | Güvenç Attila | Ice cream | Lunch only | 6 |
我们可以使用 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,z1,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 6write_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 6arrow 包允许你读写 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 6Parquet 通常比 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 文件的工作方式、可能遇到的一些问题,以及如何克服这些问题。
发表评论