7  数据导入

7.1 引言

本章介绍如何读取纯文本矩形数据文件,如何将数据写入文件,以及如何创建数据框。

主要学习readr包,同样是tidyverse的组成部分。

library(tidyverse)

7.2 从文件中读取数据

首先重点介绍最常见的矩形数据文件类型 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

使用read_csv()将文件读取到R中。其第一个参数最重要——文件路径(也可以使用URL)。

students <- read_csv("data/students.csv")
students <- read_csv("https://pos.it/r4ds-students-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

有两个问题:

  • 默认情况下,read_csv()会将空字符串""识别为 NA,但注意到原表中有个“N/A”,并未在R中显示为NA,可以单独设置将其读取为NA。
  • Student IDFull Name两个列名有引号,是因为原表中列名包含空格,不合法,除非在读取时用引号标注。
> students <- read_csv("data/students.csv", na = c("N/A", ""))
> students |> 
  rename(
    student_id = `Student ID`,
    full_name = `Full Name`
  )
> 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    <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中表示为因子(fct),而非字符(chr)。
  • age列中有一个数据为five而非数字5。
students |>
  janitor::clean_names() |>
  mutate(
    meal_plan = factor(meal_plan),
    age = parse_number(if_else(age == "five", "5", age))
  )
#> # 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

这样便基本改完了。

另外read_csv()函数可以快捷生成格式化为 CSV 文件的文本字符串:

read_csv(
  "a,b,c
  1,2,3
  4,5,6"
)
#> # A tibble: 2 × 3
#>       a     b     c
#>   <dbl> <dbl> <dbl>
#> 1     1     2     3
#> 2     4     5     6

通常,默认使用数据的第一行作为列名。但是,经常能在文件顶部看到有几行元数据,干扰列名的指定。可以使用skip = n跳过前n行,或者使用cmment = #丢弃所有以#开头的行:

read_csv(
  "The first line of metadata
  The second line of metadata
  x,y,z
  1,2,3",
  skip = 2
)
#> # A tibble: 1 × 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 × 3
#>       x     y     z
#>   <dbl> <dbl> <dbl>
#> 1     1     2     3

在某些情况下,数据可能没有列名。可以使用 col_names = FALSE来指出不要将第一行视为标题,而是从X1到Xn按顺序标记它们:

read_csv(
  "1,2,3
  4,5,6",
  col_names = FALSE
)
#> # A tibble: 2 × 3
#>      X1    X2    X3
#>   <dbl> <dbl> <dbl>
#> 1     1     2     3
#> 2     4     5     6

或者,可以传递一个字符向量给col_names,从而自定义列名:

read_csv(
  "1,2,3
  4,5,6",
  col_names = c("x", "y", "z")
)
#> # A tibble: 2 × 3
#>       x     y     z
#>   <dbl> <dbl> <dbl>
#> 1     1     2     3
#> 2     4     5     6

一旦掌握了read_csv(),其他类似函数便迎刃而解。

  • read_csv2()读取以分号分隔的文件,在用逗号作为较大位数分隔符的国家很常见。
  • read_tsv()读取制表符分隔的文件。
  • read_delim()读入包含任何分隔符的文件,自动猜测分隔符。
  • read_fwf()读取固定宽度的文件。
  • read_table()读取固定宽度文件的一种常见变体,其中列由空格分隔。
  • read_log()读取 Apache 样式的日志文件。

7.3 控制列类型

CSV文件不包含有关变量类型的信息(即它是logical、number、string 等),故readr会自己猜测类型。

但这难免会存在失误。最常见的原因是列中包含意外值,且一般会误判为chr。如果用NA之外的字符表示缺失值也称为意外值。比如:

> simple_csv <- "
  x
  10
  .
  20
  30"
> read_csv(simple_csv)
#> # A tibble: 4 × 1
#>   x    
#>   <chr>
#> 1 10   
#> 2 .    
#> 3 20   
#> 4 30

这个表很短,很快能发现预期外字符的位置,当数据特别长时需要一种特定方法。通过col_types参数自主指定每列的数据类型,然后看readr在哪报错即可。

df <- read_csv(
  simple_csv, 
  col_types = list(x = col_double()) #指定数据列的类型为双精度浮点数
)
#> Warning: One or more parsing issues, call `problems()` on your data frame for
#> details, e.g.:
#>   dat <- vroom(...)
#>   problems(dat)

现在readr指出操作存在问题,并建议我们使用problems()函数进一步确认。

problems(df)
#> # A tibble: 1 × 5
#>     row   col expected actual file                            
#>   <int> <int> <chr>    <chr>  <chr>                           
#> 1     3     1 a double .      /tmp/RtmpqR32wU/file2304111d9453

第 3 行第 1 列存在问题,其中 readr 期望得到双精度浮点数,但得到的只是一个. 。这表明此数据集使用.表示缺失值。所以设置 na = "."即可让所有意外值回到正轨。

read_csv(simple_csv, na = ".")
#> # A tibble: 4 × 1
#>       x
#>   <dbl>
#> 1    10
#> 2    NA
#> 3    20
#> 4    30

像上面col_double()一样的列类型函数共有九种:

  • col_logical()col_double()读取逻辑量和实数。
  • col_integer()读取整数。
  • col_character()读取字符串。
  • col_factor()col_date()col_datetime() 分别创建因子、日期和时间。
  • col_number()是一个数字解析器,它忽略非数字组件,对货币数据特别有用。
  • col_skip()跳过一列,使其不包含在结果中,如果有一个大型 CSV 文件并且只想使用某些列,这对于加快读取数据很有效。

除了用list()进行指定,还有cols(),且用.default参数表示所有列:

another_csv <- "
x,y,z
1,2,3"

read_csv(
  another_csv, 
  col_types = cols(.default = col_character())
)
#> # A tibble: 1 × 3
#>   x     y     z    
#>   <chr> <chr> <chr>
#> 1 1     2     3

另外还有cols_only()值得一提,它可以只读取我们指定类型的列:

read_csv(
  another_csv,
  col_types = cols_only(x = col_character())
)
#> # A tibble: 1 × 1
#>   x    
#>   <chr>
#> 1 1

7.4 多个文件读取数据

有时,数据被拆分为多个文件,而不是包含在单个文件中。如下例一次性读取:

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
#> # ℹ 13 more rows

注意到id参数为表格添加了一个指定名称的新列,该列用于标识数据来自的源文件。

7.5 文件写入

要将CSV等文件保存回磁盘,,使用write_csv()write_tsv()类型函数。有两个主要参数,一个是数据框,一个是保存的地址。

write_csv(students, "students.csv")

但是这样有个弊端。我们都知道CSV文件不包含列的类型,所以即便我们已经修改过表格,而写入后再读取仍会恢复原样。有两种解决办法保留列类型:

  • 使用write_rds()read_rds()函数。rds是以R自定义的二进制存储格式,所以我们要重新加载时的R对象与当时存储的是完全相同的。

  • arrow包中的write_parquet()read_parquet()函数。这同样是二进制存储格式,且不限于R,可以跨编程语言共享。

7.6 数据输入

有时会需要我们自己手搓一个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()。在输入数据时纵向排列,更方便排版。注意输入时列标题以~开头。
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

tribble表示:transposed tibble