library(tidyverse)
Warning: package 'ggplot2' was built under R version 4.4.2
使用R包提供的数据是学习数据科学工具的好方法,但在某个时候你会想要将所学应用到自己的数据上。在本章中,你将学习如何将数据文件读入R的基础知识。
具体来说,本章将重点介绍如何读取纯文本的矩形文件。我们将从处理列名、数据类型和缺失数据等特征的实用建议开始。然后你将学习如何一次性读取多个文件的数据,以及如何将R中的数据写入文件。最后,你将学习如何在R中手动创建数据框。
在这一章中,你将学习如何用readr包加载数据,readr是核心tidyverse的一部分。
首先,我们将关注最常见的数据文件类型:CSV,即逗号分隔值(comma-separated values)文件。
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
下面是一个简单的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 | NA |
5 | Chidiegwu Dunkel | Pizza | Breakfast and lunch | five |
6 | Güvenç Attila | Ice cream | Lunch only | 6 |
我们可以使用read_csv()将这个文件读入R。最重要的参数是文件路径。你可以把路径想象成文件的地址:这个文件名为students.csv,位于data文件夹中。如果你的项目的data文件夹中有students.csv文件,上面的代码就能工作。
你可以从https://pos.it/r4ds-students-csv 下载students.csv文件,或者直接从URL读取数据。
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.
当你运行read_csv()时,它会打印一条消息,告诉你数据的行数和列数、使用的分隔符,以及列规范(按列包含的数据类型组织的列名)。它还会打印一些关于如何获取完整列规范的信息,以及如何关闭这个消息。这个消息是readr的一个重要部分,我们将在第7.3节中再回到这个话题。
每行代表一个学生的记录,包含了这些字段的具体信息。数据中包含了一些特殊情况,比如缺失值(N/A)和可能需要清理的数据(比如”five”而不是数字5)。
当你读入数据后,第一步通常涉及对数据进行某种转换,使其在后续分析中更易于使用。让我们带着这个想法重新看看这个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”,这本应该是一个R能识别为”不可用”的真正NA值。我们可以使用na参数来解决这个问题。默认情况下,read_csv()在这个数据集中只将空字符串(““)识别为NA,而我们希望它也能将字符串”N/A”识别为NA。
# 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
你可能还注意到Student ID和Full Name这两列被反引号包围着。这是因为它们包含空格,违反了R变量名称的常规规则;它们是非语法名称。要引用这些变量,你需要用反引号将它们括起来。
# 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)。
# 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中应该表示为因子(factor)类型。要改变这个列的类型,你可以这样做:
# 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就变成了一个因子变量,这对于后续的统计分析和可视化来说更合适。通过将分类数据正确地存储为因子,你可以更好地控制它们在图形中的显示顺序,并在建模时正确处理这些变量。
注意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的值
当test为FALSE时,结果将包含第三个参数no的值
在这个例子中,我们是在说:如果age是字符串”five”,就把它变成”5”,如果不是,就保持age的原值。
这个函数在数据清理和转换中非常有用,因为它提供了一种简单的方式来基于条件修改数据。
还有一些其他重要的参数需要提到,如果我们先展示一个方便的技巧,这些会更容易演示:read_csv()可以读取你创建的并按CSV文件格式排版的文本字符串。
通常,read_csv()使用数据的第一行作为列名,这是一个很常见的惯例。但文件开头包含几行元数据也并不罕见。你可以使用skip = n来跳过前n行,或使用comment = “#”来删除所有以(例如)#开头的行。
# A tibble: 1 × 3
x y z
<dbl> <dbl> <dbl>
1 1 2 3
# A tibble: 1 × 3
x y z
<dbl> <dbl> <dbl>
1 1 2 3
在其他情况下,数据可能没有列名。你可以使用col_names = FALSE告诉read_csv()不要将第一行作为标题,而是从X1到Xn顺序标记它们。
# A tibble: 2 × 3
X1 X2 X3
<dbl> <dbl> <dbl>
1 1 2 3
2 4 5 6
另外,你可以给col_names传递一个字符向量,这将被用作列名。
# A tibble: 2 × 3
x y z
<dbl> <dbl> <dbl>
1 1 2 3
2 4 5 6
这些参数是你在实践中读取大多数CSV文件所需要知道的全部内容。(对于其余的情况,你需要仔细检查你的.csv文件并阅读read_csv()的其他参数文档。)
一旦你掌握了read_csv(),使用readr的其他函数就很简单了;关键是知道该用哪个函数:
read_csv2()用于读取分号分隔的文件。这些文件使用;而不是,来分隔字段,在使用,作为小数点的国家很常见。
read_tsv()用于读取制表符分隔的文件。
read_delim()用于读取任何分隔符的文件,如果你不指定分隔符,它会尝试自动猜测。
read_fwf()用于读取固定宽度文件。你可以通过fwf_widths()指定字段宽度,或通过fwf_positions()指定字段位置。
read_table()用于读取固定宽度文件的常见变体,其中列由空格分隔。
read_log()用于读取Apache风格的日志文件。
如果要读取用”|“分隔字段的文件,你会用什么函数?
除了file、skip和comment参数外,read_csv()和read_tsv()还有哪些共同的参数?
read_fwf()最重要的参数有哪些?
CSV文件中的字符串有时会包含逗号。为了防止这些逗号造成问题,它们需要被引号字符(如”或’)包围。默认情况下,read_csv()假设引号字符是”。要将下面的文本读入数据框,你需要指定read_csv()的什么参数?
列举下面每个内联CSV文件中的问题。当你运行代码时会发生什么?
CSV文件本身不包含每个变量类型的相关信息(比如它是逻辑型、数值型、字符串等),所以readr会尝试猜测类型。
本节介绍:
readr是如何进行类型猜测的
如何解决导致猜测失败的常见问题
必要时如何自己指定列的类型
最后,我们还会提到一些有用的通用策略。当readr猜测失败,你需要深入了解文件结构时,这些策略会很有帮助。
readr按以下顺序判断变量类型:
如果只包含F、T、FALSE或TRUE(忽略大小写)时,判定为逻辑型(logical)。
如果只包含数字(如1、-4.5、5e6、Inf)时,判定为数值型(number)。
如果匹配ISO8601标准时,判定为日期或日期时间型。(我们将在sec-creating-datetimes节中详细讨论日期时间)。
如果以上都不是,就判定为字符串型(string)。
你可以在这个简单的例子中看到这种行为:
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
")
# A tibble: 3 × 4
logical numeric date string
<lgl> <dbl> <date> <chr>
1 TRUE 1 2021-01-15 abc
2 FALSE 4.5 2021-02-15 def
3 TRUE Inf 2021-02-16 ghi
这种方法在处理整洁的数据集时效果很好,但在现实生活中,你会遇到各种奇怪且精彩的失败情况。
列类型检测最常见的失败情况是:当列中包含意外的值时,你会得到一个字符型列而不是更具体的类型。导致这种情况的最常见原因之一是缺失值的记录方式与readr预期的NA不同。
以simple为例:
没有任何其他参数时,x是一个字符型变量
在这个非常小的例子中,你可以很容易看到缺失值”.”。但如果你有数千行数据,其中只有少数几个用”.”表示的缺失值分散其中,会发生什么?一种方法是告诉readr这个x是一个数值列,然后看看它在哪里失败。你可以通过col_types参数来实现,它接受一个命名列表,列表中的名称要与CSV文件中的列名匹配。
Warning: One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
dat <- vroom(...)
problems(dat)
现在read_csv()会报告存在问题,并告诉我们可以通过problems()函数了解更多信息:
# A tibble: 1 × 5
row col expected actual file
<int> <int> <chr> <chr> <chr>
1 3 1 a double . C:/Users/Peishu/AppData/Local/Temp/RtmpCe41Cg/fil…
这告诉我们在第3行第1列有一个问题,readr期望得到一个double类型的值,但得到了一个”.”。这表明这个数据集使用”.”来表示缺失值。因此,当我们设置na = “.”后,自动猜测就会成功,给我们想要的数值列:
readr提供了九种列类型供你使用:
col_logical()和col_double()读取逻辑值和实数。这些很少需要显式指定(除了上述情况),因为readr通常会自动猜测它们。
col_integer()读取整数。在本书中我们很少区分整数和双精度数,因为它们在功能上是等效的,但明确读取整数有时很有用,因为它们只占用双精度数一半的内存。
col_character()读取字符串。当你有一个数字标识符列时,明确指定这个很有用。比如电话号码、社会安全号码、信用卡号码等这类不需要进行数学运算的长数字序列。
col_factor()、col_date()和col_datetime()分别创建因子、日期和日期时间;
col_number()是一个宽容的数值解析器,它会忽略非数值部分,特别适用于货币。
col_skip()跳过某列使其不包含在结果中,这在你有大型CSV文件且只想使用部分列时可以提高读取速度。
你也可以通过从list()切换到cols()并指定.default来覆盖默认列类型:
# A tibble: 1 × 3
x y z
<chr> <chr> <chr>
1 1 2 3
另一个有用的辅助函数是cols_only(),它只会读取你指定的列:
有时你的数据会分散在多个文件中,而不是包含在单个文件里。例如,你可能有多个月份的销售数据,每个月的数据存储在单独的文件中:
01-sales.csv 存储1月数据
02-sales.csv 存储2月数据
03-sales.csv 存储3月数据
使用read_csv(),你可以一次性读取这些数据,并将它们堆叠在一个数据框中。
sales_files <- c("data/01-sales.csv", "data/02-sales.csv", "data/03-sales.csv")
read_csv(sales_files, id = "file")
# A tibble: 19 × 6
file month year brand item n
<chr> <chr> <dbl> <dbl> <dbl> <dbl>
1 data/01-sales.csv January 2019 1 1234 3
2 data/01-sales.csv January 2019 1 8721 9
3 data/01-sales.csv January 2019 1 1822 2
4 data/01-sales.csv January 2019 2 3333 1
5 data/01-sales.csv January 2019 2 2156 9
6 data/01-sales.csv January 2019 2 3987 6
7 data/01-sales.csv January 2019 2 3827 6
8 data/02-sales.csv February 2019 1 1234 8
9 data/02-sales.csv February 2019 1 8721 2
10 data/02-sales.csv February 2019 1 1822 3
11 data/02-sales.csv February 2019 2 3333 1
12 data/02-sales.csv February 2019 2 2156 3
13 data/02-sales.csv February 2019 2 3987 6
14 data/03-sales.csv March 2019 1 1234 3
15 data/03-sales.csv March 2019 1 3627 1
16 data/03-sales.csv March 2019 1 8820 3
17 data/03-sales.csv March 2019 2 7253 1
18 data/03-sales.csv March 2019 2 8766 3
19 data/03-sales.csv March 2019 2 8288 6
再次说明,如果你的项目的data文件夹中有这些CSV文件,上述代码就能正常工作。你可以从以下链接下载这些文件,或者你可以直接从网络读取这些文件。
id参数会在结果数据框中添加一个名为file的新列,用于标识数据来自哪个文件。这在你读取的文件中没有可以帮助追踪数据来源的标识列时特别有用。
readr还提供了两个用于将数据写回硬盘的有用函数:write_csv()和write_tsv()。这些函数最重要的参数是x(要保存的数据框)和file(保存的位置)。你还可以通过na参数指定如何写入缺失值,以及是否要将数据追加到现有文件中。
现在让我们重新读取那个CSV文件。注意,当你保存为CSV文件时,你之前设置的变量类型信息会丢失,因为你又要从一个纯文本文件重新开始读取了。
# 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
# 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对象。
# 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()函数是按列工作的:
# 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()(transposed tibble的缩写),它允许你按行布局数据。tribble()是为代码中的数据输入而设计的:列标题以~开头,数据项用逗号分隔。这使得以易读的形式布局少量数据成为可能:
在本章中,你学习了如何使用read_csv()加载CSV文件,以及如何使用tibble()和tribble()进行数据输入。你了解了CSV文件的工作原理、可能遇到的一些问题以及如何解决这些问题。在本书中,我们还会多次涉及数据导入的话题:从Excel和Google表格导入数据,从数据库加载数据,从parquet文件导入,@sec-介绍从JSON导入,以及从网站导入数据。
我们即将结束本书的这一部分,但还有一个重要的主题需要讨论:如何获取帮助。因此,在下一章中,你将学习一些寻求帮助的好去处,如何创建reprex(可重现示例)以最大限度地提高获得好帮助的机会,以及一些关于如何跟上R语言发展的建议。