数据获取与预处理

Published

September 8, 2025

本部分介绍数据导入与获取、数据转化和清理。

1 数据导入与获取

R 几乎可以导入任何格式的数据,包括文本、Excel、SPSS和STATA等统计软件等格式的数据,同时R还能够连接数据库获取各种类型的数据。

1.1 文本文件

readr包提供了将分隔文本文件导入 R 数据框的函数选择。

library(readr)

# 导入逗号分割的数据
cgss2017 <- read_csv("cgss2017.csv")

# 导入制表符分割的数据
cgss2017 <- read_tsv("cgss2017.txt")

1.2 Excel文件

readxl包可以从 Excel 文件导入数据,支持 xls 和 xlsx 格式。

library(readxl)

# 从excel工作表导入数据
cgss2017 <- read_excel("cgss2017.xlsx", sheet=1)

由于excel文件可以包含多个工作表,因此您可以使用sheet选项指定所需的工作表。默认值为sheet=1。

1.3 专业统计软件数据文件

haven包提供了从各种统计包导入数据的功能。示例采用的CGSS2015子数据集

library(haven)

# 导入stata数据
cgss2017 <- read_dta("cgss2017.dta")

# 导入SPSS数据
cgss <- read_sav("cgss2015subset.sav")

# 导入SAS数据
cgss2017 <- read_sas("cgss2017.sas7bdat")

1.4 数据库

本部分介绍如何从数据库获取数据。数据库操作用到的包中,DBI 是连接数据库并执行 SQL(结构化查询语言) 的低级接口;dbplyr 是高级接口,将 dplyr 代码转换为 SQL 查询,然后使用 DBI 执行。

library(DBI)
library(dbplyr)
library(tidyverse)

数据库可以看做数据框(data frame)的集合,在数据库术语中称为表(table)。数据库表存储在磁盘上,大小可以任意。数据库表有索引,可以快速找到感兴趣的行。数据库针对快速收集数据而非分析现有数据进行了优化。

数据库由数据库管理系统(简称DBMS )运行,数据库管理系统有三种基本形式,包括客户端-服务器型( 运行在中央服务器上,通过客户端连接,例如PostgreSQL、MariaDB、SQL Server 和 Oracle)、云端数据库(例如亚马逊的 RedShift 和谷歌的 BigQuery)、进程内数据库(例如 SQLite 或 duckdb,完全在个人计算机上)。

DBI基础

# 连接到duckdb,命令会创建一个临时数据库
# 如需建立永久数据库,可设置保存文件夹
con <- DBI::dbConnect(duckdb::duckdb())

# 从其他包加载数据创建数据库表
dbWriteTable(con, "mpg", ggplot2::mpg)
dbWriteTable(con, "diamonds", ggplot2::diamonds)

# 检查数据库中的表
dbListTables(con)
[1] "diamonds" "mpg"     
# 检查表diamonds中的内容
con |> 
  dbReadTable("diamonds") |> 
  as_tibble()
# A tibble: 53,940 × 10
   carat cut       color clarity depth table price     x     y     z
   <dbl> <fct>     <fct> <fct>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
 1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
 2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
 3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
 4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
 5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
 6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
 7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
 8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
 9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
# ℹ 53,930 more rows
# 采用SQL对数据库进行查询,并获取数据
sql <- "
  SELECT carat, cut, clarity, color, price 
  FROM diamonds 
  WHERE price > 15000
"
as_tibble(dbGetQuery(con, sql))
# A tibble: 1,655 × 5
   carat cut       clarity color price
   <dbl> <fct>     <fct>   <fct> <int>
 1  1.54 Premium   VS2     E     15002
 2  1.19 Ideal     VVS1    F     15005
 3  2.1  Premium   SI1     I     15007
 4  1.69 Ideal     SI1     D     15011
 5  1.5  Very Good VVS2    G     15013
 6  1.73 Very Good VS1     G     15014
 7  2.02 Premium   SI2     G     15014
 8  2.05 Very Good SI2     F     15017
 9  1.5  Very Good VS1     F     15022
10  1.82 Very Good SI1     G     15025
# ℹ 1,645 more rows

dbplyr基础

dbplyr 是 dplyr 的后端,意味着可以通过 dplyr 函数代码来实现SQL操作。

# 生成一个数据库表对象
diamonds_db <- tbl(con, "diamonds")
diamonds_db
# Source:   table<diamonds> [?? x 10]
# Database: DuckDB v1.3.2 [liangdan@Darwin 24.6.0:R 4.4.2/:memory:]
   carat cut       color clarity depth table price     x     y     z
   <dbl> <fct>     <fct> <fct>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
 1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
 2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
 3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
 4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
 5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
 6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
 7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
 8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
 9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
# ℹ more rows
big_diamonds_db <- diamonds_db |> 
  filter(price > 15000) |> 
  select(carat:clarity, price)

big_diamonds_db
# Source:   SQL [?? x 5]
# Database: DuckDB v1.3.2 [liangdan@Darwin 24.6.0:R 4.4.2/:memory:]
   carat cut       color clarity price
   <dbl> <fct>     <fct> <fct>   <int>
 1  1.54 Premium   E     VS2     15002
 2  1.19 Ideal     F     VVS1    15005
 3  2.1  Premium   I     SI1     15007
 4  1.69 Ideal     D     SI1     15011
 5  1.5  Very Good G     VVS2    15013
 6  1.73 Very Good G     VS1     15014
 7  2.02 Premium   G     SI2     15014
 8  2.05 Very Good F     SI2     15017
 9  1.5  Very Good F     VS1     15022
10  1.82 Very Good G     SI1     15025
# ℹ more rows

该对象只代表一个数据库查询,并不是完整数据,顶部显示了 DBMS 名称,并且只有列数,没有行数。因为查找总行数需要执行完整的查询,而这正是在处理大数据时要避免的,执行完整查询会消耗计算资源。

# 展示dplyr代码所实现的SQL
big_diamonds_db |>
  show_query()
<SQL>
SELECT carat, cut, color, clarity, price
FROM diamonds
WHERE (price > 15000.0)

获取完整数据

big_diamonds <- big_diamonds_db |> 
  collect()
big_diamonds
# A tibble: 1,655 × 5
   carat cut       color clarity price
   <dbl> <fct>     <fct> <fct>   <int>
 1  1.54 Premium   E     VS2     15002
 2  1.19 Ideal     F     VVS1    15005
 3  2.1  Premium   I     SI1     15007
 4  1.69 Ideal     D     SI1     15011
 5  1.5  Very Good G     VVS2    15013
 6  1.73 Very Good G     VS1     15014
 7  2.02 Premium   G     SI2     15014
 8  2.05 Very Good F     SI2     15017
 9  1.5  Very Good F     VS1     15022
10  1.82 Very Good G     SI1     15025
# ℹ 1,645 more rows

SQL基础

SQL由语句组成,例如CREATE、INSERT、SELECT等。查询语句SELECT由5个子句组成,SELECT决定选择哪些列或变量;FROM指明数据库表;WHERE决定选择哪些行;ORDER BY决定如何排序;GROUP BY将查询进行汇总聚合(类似分类汇总)。

# 获取数据,建立数据表对象
dbplyr::copy_nycflights13(con)
flights <- tbl(con, "flights")
planes <- tbl(con, "planes")

flights |> show_query()
<SQL>
SELECT *
FROM flights
flights |> 
  filter(dest == "IAH") |> 
  arrange(dep_delay) |>
  show_query()
<SQL>
SELECT flights.*
FROM flights
WHERE (dest = 'IAH')
ORDER BY dep_delay
flights |> 
  group_by(dest) |> 
  summarize(dep_delay = mean(dep_delay, na.rm = TRUE)) |> 
  show_query()
<SQL>
SELECT dest, AVG(dep_delay) AS dep_delay
FROM flights
GROUP BY dest

SQL的表连接与dplyr的数据框连接类似。

flights |> 
  left_join(planes |> rename(year_built = year), join_by(tailnum)) |> 
  show_query()
<SQL>
SELECT
  flights.*,
  planes."year" AS year_built,
  "type",
  manufacturer,
  model,
  engines,
  seats,
  speed,
  engine
FROM flights
LEFT JOIN planes
  ON (flights.tailnum = planes.tailnum)

1.4 Arrow 工具

Parquet 格式是一种基于开放标准的格式,被大数据系统广泛使用替代csv格式。Apache Arrow是为高效分析和传输大数据而设计的工具箱,处理速度极快。R 语言提供了 arrow 包来使用 Apache Arrow ,该包提供了一个 dplyr 后端,允许使用熟悉的 dplyr 语法分析大于内存的数据集。

library(tidyverse)
library(arrow)

library(dbplyr, warn.conflicts = FALSE)
library(duckdb)

数据为西雅图公共图书馆书籍借阅记录的CSV文件,数据共有41,389,465行,显示从2005-2022年每本书每月的借阅次数,数据大小为9GB。代码仅为演示。

dir.create("data", showWarnings = FALSE)

# 下载西雅图公共图书馆书籍借阅记录
# 数据共有41,389,465行,显示从2005-2022年每本书每月的借阅次数
curl::multi_download(
  "https://r4ds.s3.us-west-2.amazonaws.com/seattle-library-checkouts.csv",
  "data/seattle-library-checkouts.csv",
  resume = TRUE
)
#> # A tibble: 1 × 10
#>   success status_code resumefrom url                    destfile        error
#>   <lgl>         <int>      <dbl> <chr>                  <chr>           <chr>
#> 1 TRUE            200          0 https://r4ds.s3.us-we… data/seattle-l… <NA> 
#> # ℹ 4 more variables: type <chr>, modified <dttm>, time <dbl>,
#> #   headers <list>

打开数据集

seattle_csv <- open_dataset(
  sources = "data/seattle-library-checkouts.csv", 
  # 函数会自动扫描几千行数据猜测数据结构
  # 但由于部分书籍ISBN为空,直接指定该列的数据类型
  col_types = schema(ISBN = string()),
  format = "csv"
)

命令并未读取全部数据,数据仍在磁盘上,等待需要时才加载到内存。可以展现文件的元数据信息。

seattle_csv
#> FileSystemDataset with 1 csv file
#> UsageClass: string
#> CheckoutType: string
#> MaterialType: string
#> CheckoutYear: int64
#> CheckoutMonth: int64
#> Checkouts: int64
#> Title: string
#> ISBN: string
#> Creator: string
#> Subjects: string
#> Publisher: string
#> PublicationYear: string

查看文件内容

seattle_csv |> glimpse()
#> FileSystemDataset with 1 csv file
#> 41,389,465 rows x 12 columns
#> $ UsageClass      <string> "Physical", "Physical", "Digital", "Physical", "Ph…
#> $ CheckoutType    <string> "Horizon", "Horizon", "OverDrive", "Horizon", "Hor…
#> $ MaterialType    <string> "BOOK", "BOOK", "EBOOK", "BOOK", "SOUNDDISC", "BOO…
#> $ CheckoutYear     <int64> 2016, 2016, 2016, 2016, 2016, 2016, 2016, 2016, 20…
#> $ CheckoutMonth    <int64> 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,…
#> $ Checkouts        <int64> 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 1, 2, 3, 2, 1, 3, 2,…
#> $ Title           <string> "Super rich : a guide to having it all / Russell S…
#> $ ISBN            <string> "", "", "", "", "", "", "", "", "", "", "", "", ""…
#> $ Creator         <string> "Simmons, Russell", "Barclay, James, 1965-", "Tim …
#> $ Subjects        <string> "Self realization, Conduct of life, Attitude Psych…
#> $ Publisher       <string> "Gotham Books,", "Pyr,", "Random House, Inc.", "Di…
#> $ PublicationYear <string> "c2011.", "2010.", "2015", "2005.", "c2004.", "c20…

获取每年的借阅总数

seattle_csv |> 
  group_by(CheckoutYear) |> 
  summarise(Checkouts = sum(Checkouts)) |> 
  arrange(CheckoutYear) |> 
  collect()
#> # A tibble: 18 × 2
#>   CheckoutYear Checkouts
#>          <int>     <int>
#> 1         2005   3798685
#> 2         2006   6599318
#> 3         2007   7126627
#> 4         2008   8438486
#> 5         2009   9135167
#> 6         2010   8608966
#> # ℹ 12 more rows

采用Parquet格式进行分区

Parquet 是专为大数据需求而设计的自定义二进制格式。这意味着通常等效的 CSV 文件更小,访问速度更快。Parquet 文件拥有丰富的类型系统,能够将数据类型与数据一起记录(类似SPSS的SAV文件)。Parquet 文件是按列组织的,类似于 R 的数据框,便于数据分析。Parquet 文件是分区块的,这使得可以同时处理文件的不同部分,有时可以完全跳过一些块。

# 指定分区存储路径
pq_path <- "data/seattle-library-checkouts"

# 按照借阅年份进行分区,分成18个区块
seattle_csv |>
  group_by(CheckoutYear) |>
  write_dataset(path = pq_path, format = "parquet")

# 展示分好的区块
tibble(
  files = list.files(pq_path, recursive = TRUE),
  size_MB = file.size(file.path(pq_path, files)) / 1024^2
)
#> # A tibble: 18 × 2
#>   files                            size_MB
#>   <chr>                              <dbl>
#> 1 CheckoutYear=2005/part-0.parquet    109.
#> 2 CheckoutYear=2006/part-0.parquet    164.
#> 3 CheckoutYear=2007/part-0.parquet    178.
#> 4 CheckoutYear=2008/part-0.parquet    195.
#> 5 CheckoutYear=2009/part-0.parquet    214.
#> 6 CheckoutYear=2010/part-0.parquet    222.
#> # ℹ 12 more rows

每个区块大小在100到300MB之间,总共4GB,远小于CSV文件的大小。

使用Arrow支持的dplyr

seattle_pq <- open_dataset(pq_path)

# 统计过去五年内每月借出的图书总数
query <- seattle_pq |> 
  filter(CheckoutYear >= 2018, MaterialType == "BOOK") |>
  group_by(CheckoutYear, CheckoutMonth) |>
  summarize(TotalCheckouts = sum(Checkouts)) |>
  arrange(CheckoutYear, CheckoutMonth)

# 获取数据结果
query |> collect()
#> # A tibble: 58 × 3
#> # Groups:   CheckoutYear [5]
#>   CheckoutYear CheckoutMonth TotalCheckouts
#>          <int>         <int>          <int>
#> 1         2018             1         355101
#> 2         2018             2         309813
#> 3         2018             3         344487
#> 4         2018             4         330988
#> 5         2018             5         318049
#> 6         2018             6         341825
#> # ℹ 52 more rows

效率比较

# 不分区的运行时间
seattle_csv |> 
  filter(CheckoutYear == 2021, MaterialType == "BOOK") |>
  group_by(CheckoutMonth) |>
  summarize(TotalCheckouts = sum(Checkouts)) |>
  arrange(desc(CheckoutMonth)) |>
  collect() |> 
  system.time()
#>    user  system elapsed 
#>  11.951   1.297  11.387

# 分区后的运行时间
seattle_pq |> 
  filter(CheckoutYear == 2021, MaterialType == "BOOK") |>
  group_by(CheckoutMonth) |>
  summarize(TotalCheckouts = sum(Checkouts)) |>
  arrange(desc(CheckoutMonth)) |>
  collect() |> 
  system.time()
#>    user  system elapsed 
#>   0.263   0.058   0.063  

分区后,效率提升了100倍。

1.5 分层数据

本部分先介绍R语言中的另一个重要对象list,以及通过list构建分层数据解决实际问题,此外,介绍网络数据的常见格式JSON文件。

library(tidyverse)
library(repurrrsive)
library(jsonlite)

列表

列表可以在同一个向量中存储不同类型的元素。

x1 <- list(1:4, "a", TRUE)
x1
[[1]]
[1] 1 2 3 4

[[2]]
[1] "a"

[[3]]
[1] TRUE
# 展示列表的结构
str(x1)
List of 3
 $ : int [1:4] 1 2 3 4
 $ : chr "a"
 $ : logi TRUE
x2 <- list(1, list(2, list(3, list(4, list(5)))))

# 如果列表结构过于复杂,可以采用View()进行交互探索
View(x2)

列表列是由列表(list)构成的列数据(column),而不是常见的变量。列表列便于存储分析模型的输出结果(例如,回归分析获得的系数和标准误等)、地理数据(地图对象多边形的坐标点对)等非常规数据类型。

df <- tibble(
  x = 1:2, 
  y = c("a", "b"),
  z = list(list(1, 2), list(3, 4, 5))
)
df
# A tibble: 2 × 3
      x y     z         
  <int> <chr> <list>    
1     1 a     <list [2]>
2     2 b     <list [3]>

解除列表列的嵌套

列表列的数据不便于计算与分析,常需要先进行解除嵌套操作。

  • 元素具有命名的列表列(或列表等长度)
df1 <- tribble(
  ~x, ~y,
  1, list(a = 11, b = 12),
  2, list(a = 21, b = 22),
  3, list(a = 31, b = 32),
)

df1 |> 
  unnest_wider(y)
# A tibble: 3 × 3
      x     a     b
  <dbl> <dbl> <dbl>
1     1    11    12
2     2    21    22
3     3    31    32
  • 元素未命名的列表列(通常列表长度不等)
df2 <- tribble(
  ~x, ~y,
  1, list(11, 12, 13),
  2, list(21),
  3, list(31, 32),
)

df2 |> 
  unnest_longer(y)
# A tibble: 6 × 2
      x     y
  <dbl> <dbl>
1     1    11
2     1    12
3     1    13
4     2    21
5     3    31
6     3    32
  • 宽数据

有些层次数据的结构中含有多个嵌套,并且多次解除嵌套后的变量列非常长。

# 获取层次数据,并查看结构
repos <- tibble(json = gh_repos)
repos
# A tibble: 6 × 1
  json       
  <list>     
1 <list [30]>
2 <list [30]>
3 <list [30]>
4 <list [26]>
5 <list [30]>
6 <list [30]>

数据为6行1列,列表列为不等长的未命名列表

# 解除嵌套,将每个列表元素列为单独一行
repos |> 
  unnest_longer(json)
# A tibble: 176 × 1
   json             
   <list>           
 1 <named list [68]>
 2 <named list [68]>
 3 <named list [68]>
 4 <named list [68]>
 5 <named list [68]>
 6 <named list [68]>
 7 <named list [68]>
 8 <named list [68]>
 9 <named list [68]>
10 <named list [68]>
# ℹ 166 more rows

解除完依然是列表,但是获得等长的命名列表。

# 解除嵌套,将每个列表元素置入相应列
repos |> 
  unnest_longer(json) |> 
  unnest_wider(json) 
# A tibble: 176 × 68
        id name  full_name owner        private html_url description fork  url  
     <int> <chr> <chr>     <list>       <lgl>   <chr>    <chr>       <lgl> <chr>
 1  6.12e7 after gaborcsa… <named list> FALSE   https:/… Run Code i… FALSE http…
 2  4.05e7 argu… gaborcsa… <named list> FALSE   https:/… Declarativ… FALSE http…
 3  3.64e7 ask   gaborcsa… <named list> FALSE   https:/… Friendly C… FALSE http…
 4  3.49e7 base… gaborcsa… <named list> FALSE   https:/… Do we get … FALSE http…
 5  6.16e7 cite… gaborcsa… <named list> FALSE   https:/… Test R pac… TRUE  http…
 6  3.39e7 clis… gaborcsa… <named list> FALSE   https:/… Unicode sy… FALSE http…
 7  3.72e7 cmak… gaborcsa… <named list> FALSE   https:/… port of cm… TRUE  http…
 8  6.80e7 cmark gaborcsa… <named list> FALSE   https:/… CommonMark… TRUE  http…
 9  6.32e7 cond… gaborcsa… <named list> FALSE   https:/… <NA>        TRUE  http…
10  2.43e7 cray… gaborcsa… <named list> FALSE   https:/… R package … FALSE http…
# ℹ 166 more rows
# ℹ 59 more variables: forks_url <chr>, keys_url <chr>,
#   collaborators_url <chr>, teams_url <chr>, hooks_url <chr>,
#   issue_events_url <chr>, events_url <chr>, assignees_url <chr>,
#   branches_url <chr>, tags_url <chr>, blobs_url <chr>, git_tags_url <chr>,
#   git_refs_url <chr>, trees_url <chr>, statuses_url <chr>,
#   languages_url <chr>, stargazers_url <chr>, contributors_url <chr>, …

得到68列,挑选一些列查看。

repos |> 
  unnest_longer(json) |> 
  unnest_wider(json) |> 
  select(id, full_name, owner, description)
# A tibble: 176 × 4
         id full_name               owner             description               
      <int> <chr>                   <list>            <chr>                     
 1 61160198 gaborcsardi/after       <named list [17]> Run Code in the Background
 2 40500181 gaborcsardi/argufy      <named list [17]> Declarative function argu…
 3 36442442 gaborcsardi/ask         <named list [17]> Friendly CLI interaction …
 4 34924886 gaborcsardi/baseimports <named list [17]> Do we get warnings for un…
 5 61620661 gaborcsardi/citest      <named list [17]> Test R package and repo f…
 6 33907457 gaborcsardi/clisymbols  <named list [17]> Unicode symbols for CLI a…
 7 37236467 gaborcsardi/cmaker      <named list [17]> port of cmake to r        
 8 67959624 gaborcsardi/cmark       <named list [17]> CommonMark parsing and re…
 9 63152619 gaborcsardi/conditions  <named list [17]> <NA>                      
10 24343686 gaborcsardi/crayon      <named list [17]> R package for colored ter…
# ℹ 166 more rows

对owner进行解除嵌套。

repos |> 
  unnest_longer(json) |> 
  unnest_wider(json) |> 
  select(id, full_name, owner, description) |> 
  unnest_wider(owner, names_sep = "_")
# A tibble: 176 × 20
         id full_name    owner_login owner_id owner_avatar_url owner_gravatar_id
      <int> <chr>        <chr>          <int> <chr>            <chr>            
 1 61160198 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 2 40500181 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 3 36442442 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 4 34924886 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 5 61620661 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 6 33907457 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 7 37236467 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 8 67959624 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 9 63152619 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
10 24343686 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
# ℹ 166 more rows
# ℹ 14 more variables: owner_url <chr>, owner_html_url <chr>,
#   owner_followers_url <chr>, owner_following_url <chr>,
#   owner_gists_url <chr>, owner_starred_url <chr>,
#   owner_subscriptions_url <chr>, owner_organizations_url <chr>,
#   owner_repos_url <chr>, owner_events_url <chr>,
#   owner_received_events_url <chr>, owner_type <chr>, …
  • 关系型数据

权力的游戏中的角色信息

chars <- tibble(json = got_chars)
chars
# A tibble: 30 × 1
   json             
   <list>           
 1 <named list [18]>
 2 <named list [18]>
 3 <named list [18]>
 4 <named list [18]>
 5 <named list [18]>
 6 <named list [18]>
 7 <named list [18]>
 8 <named list [18]>
 9 <named list [18]>
10 <named list [18]>
# ℹ 20 more rows

按命名列表列解除嵌套

chars |> 
  unnest_wider(json)
# A tibble: 30 × 18
   url           id name  gender culture born  died  alive titles aliases father
   <chr>      <int> <chr> <chr>  <chr>   <chr> <chr> <lgl> <list> <list>  <chr> 
 1 https://w…  1022 Theo… Male   "Ironb… "In … ""    TRUE  <chr>  <chr>   ""    
 2 https://w…  1052 Tyri… Male   ""      "In … ""    TRUE  <chr>  <chr>   ""    
 3 https://w…  1074 Vict… Male   "Ironb… "In … ""    TRUE  <chr>  <chr>   ""    
 4 https://w…  1109 Will  Male   ""      ""    "In … FALSE <chr>  <chr>   ""    
 5 https://w…  1166 Areo… Male   "Norvo… "In … ""    TRUE  <chr>  <chr>   ""    
 6 https://w…  1267 Chett Male   ""      "At … "In … FALSE <chr>  <chr>   ""    
 7 https://w…  1295 Cres… Male   ""      "In … "In … FALSE <chr>  <chr>   ""    
 8 https://w…   130 Aria… Female "Dorni… "In … ""    TRUE  <chr>  <chr>   ""    
 9 https://w…  1303 Daen… Female "Valyr… "In … ""    TRUE  <chr>  <chr>   ""    
10 https://w…  1319 Davo… Male   "Weste… "In … ""    TRUE  <chr>  <chr>   ""    
# ℹ 20 more rows
# ℹ 7 more variables: mother <chr>, spouse <chr>, allegiances <list>,
#   books <list>, povBooks <list>, tvSeries <list>, playedBy <list>

检查列表列

chars |> 
  unnest_wider(json) |> 
  select(id, where(is.list))
# A tibble: 30 × 8
      id titles    aliases    allegiances books     povBooks  tvSeries  playedBy
   <int> <list>    <list>     <list>      <list>    <list>    <list>    <list>  
 1  1022 <chr [2]> <chr [4]>  <chr [1]>   <chr [3]> <chr [2]> <chr [6]> <chr>   
 2  1052 <chr [2]> <chr [11]> <chr [1]>   <chr [2]> <chr [4]> <chr [6]> <chr>   
 3  1074 <chr [2]> <chr [1]>  <chr [1]>   <chr [3]> <chr [2]> <chr [1]> <chr>   
 4  1109 <chr [1]> <chr [1]>  <NULL>      <chr [1]> <chr [1]> <chr [1]> <chr>   
 5  1166 <chr [1]> <chr [1]>  <chr [1]>   <chr [3]> <chr [2]> <chr [2]> <chr>   
 6  1267 <chr [1]> <chr [1]>  <NULL>      <chr [2]> <chr [1]> <chr [1]> <chr>   
 7  1295 <chr [1]> <chr [1]>  <NULL>      <chr [2]> <chr [1]> <chr [1]> <chr>   
 8   130 <chr [1]> <chr [1]>  <chr [1]>   <chr [4]> <chr [1]> <chr [1]> <chr>   
 9  1303 <chr [5]> <chr [11]> <chr [1]>   <chr [1]> <chr [4]> <chr [6]> <chr>   
10  1319 <chr [4]> <chr [5]>  <chr [2]>   <chr [1]> <chr [3]> <chr [5]> <chr>   
# ℹ 20 more rows

对称号李表列进行解除嵌套,解除后的数据可以采用连接(join)与主数据合并成结构化数据框。

titles <- chars |> 
  unnest_wider(json) |> 
  select(id, titles) |> 
  unnest_longer(titles) |> 
  filter(titles != "") |> 
  rename(title = titles)
titles
# A tibble: 52 × 2
      id title                                                                  
   <int> <chr>                                                                  
 1  1022 Prince of Winterfell                                                   
 2  1022 Lord of the Iron Islands (by law of the green lands)                   
 3  1052 Acting Hand of the King (former)                                       
 4  1052 Master of Coin (former)                                                
 5  1074 Lord Captain of the Iron Fleet                                         
 6  1074 Master of the Iron Victory                                             
 7  1166 Captain of the Guard at Sunspear                                       
 8  1295 Maester                                                                
 9   130 Princess of Dorne                                                      
10  1303 Queen of the Andals and the Rhoynar and the First Men, Lord of the Sev…
# ℹ 42 more rows
  • 深度嵌套数据
gmaps_cities
# A tibble: 5 × 2
  city       json            
  <chr>      <list>          
1 Houston    <named list [2]>
2 Washington <named list [2]>
3 New York   <named list [2]>
4 Chicago    <named list [2]>
5 Arlington  <named list [2]>

数据为5行,5个城市名称和google geocoding API返回的地理信息列表列

gmaps_cities |> 
  unnest_wider(json)
# A tibble: 5 × 3
  city       results    status
  <chr>      <list>     <chr> 
1 Houston    <list [1]> OK    
2 Washington <list [2]> OK    
3 New York   <list [1]> OK    
4 Chicago    <list [1]> OK    
5 Arlington  <list [2]> OK    
# 剔除status列后继续对未命名列表进行解除嵌套
gmaps_cities |> 
  unnest_wider(json) |> 
  select(-status) |> 
  unnest_longer(results)
# A tibble: 7 × 2
  city       results         
  <chr>      <list>          
1 Houston    <named list [5]>
2 Washington <named list [5]>
3 Washington <named list [5]>
4 New York   <named list [5]>
5 Chicago    <named list [5]>
6 Arlington  <named list [5]>
7 Arlington  <named list [5]>

继续解除results列表列的嵌套,得到地理信息列表列geometry

locations <- gmaps_cities |> 
  unnest_wider(json) |> 
  select(-status) |> 
  unnest_longer(results) |> 
  unnest_wider(results)
locations
# A tibble: 7 × 6
  city       address_components formatted_address   geometry     place_id types 
  <chr>      <list>             <chr>               <list>       <chr>    <list>
1 Houston    <list [4]>         Houston, TX, USA    <named list> ChIJAYW… <list>
2 Washington <list [2]>         Washington, USA     <named list> ChIJ-bD… <list>
3 Washington <list [4]>         Washington, DC, USA <named list> ChIJW-T… <list>
4 New York   <list [3]>         New York, NY, USA   <named list> ChIJOwg… <list>
5 Chicago    <list [4]>         Chicago, IL, USA    <named list> ChIJ7cv… <list>
6 Arlington  <list [4]>         Arlington, TX, USA  <named list> ChIJ05g… <list>
7 Arlington  <list [4]>         Arlington, VA, USA  <named list> ChIJD6e… <list>

对地理几何体解除嵌套后,得到城市边界(bounds)和位置(location)的列表列

locations |> 
  select(city, formatted_address, geometry) |> 
  unnest_wider(geometry)
# A tibble: 7 × 6
  city    formatted_address bounds       location     location_type viewport    
  <chr>   <chr>             <list>       <list>       <chr>         <list>      
1 Houston Houston, TX, USA  <named list> <named list> APPROXIMATE   <named list>
2 Washin… Washington, USA   <named list> <named list> APPROXIMATE   <named list>
3 Washin… Washington, DC, … <named list> <named list> APPROXIMATE   <named list>
4 New Yo… New York, NY, USA <named list> <named list> APPROXIMATE   <named list>
5 Chicago Chicago, IL, USA  <named list> <named list> APPROXIMATE   <named list>
6 Arling… Arlington, TX, U… <named list> <named list> APPROXIMATE   <named list>
7 Arling… Arlington, VA, U… <named list> <named list> APPROXIMATE   <named list>

继续对location进行解除嵌套,可以得到城市的经纬度坐标。

locations |> 
  select(city, formatted_address, geometry) |> 
  unnest_wider(geometry) |> 
  unnest_wider(location)
# A tibble: 7 × 7
  city    formatted_address bounds         lat    lng location_type viewport    
  <chr>   <chr>             <list>       <dbl>  <dbl> <chr>         <list>      
1 Houston Houston, TX, USA  <named list>  29.8  -95.4 APPROXIMATE   <named list>
2 Washin… Washington, USA   <named list>  47.8 -121.  APPROXIMATE   <named list>
3 Washin… Washington, DC, … <named list>  38.9  -77.0 APPROXIMATE   <named list>
4 New Yo… New York, NY, USA <named list>  40.7  -74.0 APPROXIMATE   <named list>
5 Chicago Chicago, IL, USA  <named list>  41.9  -87.6 APPROXIMATE   <named list>
6 Arling… Arlington, TX, U… <named list>  32.7  -97.1 APPROXIMATE   <named list>
7 Arling… Arlington, VA, U… <named list>  38.9  -77.1 APPROXIMATE   <named list>

感兴趣可以继续对城市边界(bounds)进行操作。在空间数据分析部分会继续介绍地理数据。

1.6 JSON

JSON(JavaScript Object Notation)是大多数网页API返回的数据格式。JSON的数据类型包括null(与R的NA类似)、字符串(需用双引号)、数字、布尔值、数组和对象。数组和对象都类似于 R 中的列表;区别在于它们是否命名。 数组(类似未命名列表):[1, 2, 3] 、[null, 1, “string”, false] 对象(类似命名列表):{“x”: 1, “y”: 2}

jsonlite包可以将JSON转换为R数据结构。read_json()从磁盘读取JSON文件,parse_json()进行转化。

str(parse_json('1'))
 int 1
str(parse_json('[1, 2, 3]'))
List of 3
 $ : int 1
 $ : int 2
 $ : int 3
str(parse_json('{"x": [1, 2, 3]}'))
List of 1
 $ x:List of 3
  ..$ : int 1
  ..$ : int 2
  ..$ : int 3

常见JSON文件只包含一个顶层数组(意味着代表多个事物,例如返回多个网页、多个记录、多个结果),可以直接转化

# 网页API返回的人员年龄数据
json <- '[
  {"name": "张三", "age": 34},
  {"name": "李四", "age": 27}
]'
df <- tibble(json = parse_json(json))
df |> 
  unnest_wider(json)
# A tibble: 2 × 2
  name    age
  <chr> <int>
1 张三     34
2 李四     27

偶尔JSON文件包含一个顶级对象,需要对其先包装到列表,再进行转化。

# 返回结果里包含了本次请求是否成功的信息
json <- '{
  "status": "OK", 
  "results": [
    {"name": "John", "age": 34},
    {"name": "Susan", "age": 27}
 ]
}
'
df <- tibble(json = list(parse_json(json)))
df |>
  unnest_wider(json) |> 
  unnest_longer(results) |> 
  unnest_wider(results)
# A tibble: 2 × 3
  status name    age
  <chr>  <chr> <int>
1 OK     John     34
2 OK     Susan    27

1.7 网页爬取

R语言可以承担一些简单的网页爬取工作,并且可以很方便的进行分析,但是具有反爬虫机制的动态网页还需更专业的爬虫工具,例如Python等。

HTML基础

HTML是一种描述网页的语言,全称是超文本标记语言( Hyper Text Markup Language )。

<html>
<head>
  <title>Page title</title>
</head>
<body>
  <h1 id='first'>A heading</h1>
  <p>Some text &amp; <b>some bold text.</b></p>
  <img src='myimg.png' width='100' height='100'>
</body>

HTML 是层次结构的文件,包括开始标记(例如<tag>),可选属性(id='first'),结束标记(如</tag>)和内容(开始标记和结束标记之间的所有内容)。

HTML 元素有 100 多个。最重要的元素是:

  • 每个 HTML 页面都必须包含在一个<html>元素中,并且该元素必须具有两个子元素:<head>,其中包含页面标题等文档元数据;以及<body>,其中包含在浏览器上看到的内容。

  • <h1>(标题 1)、<section>(部分)、<p>(段落)和<ol>(有序列表)等块标签构成了页面的整体结构。

  • 内联标签(例如<b>(粗体)、<i>(斜体)和<a>(链接))格式化块标签内的文本。

标签可以具有类似于 name1='value1' name2='value2' 之类的属性。其中两个最重要的属性是id和class,它们与 CSS(层叠样式表)结合使用,以控制页面的视觉外观。这些属性在从页面抓取数据时通常很有用。属性还用于记录链接的目标(元素href的属性<a>)和图像的来源(元素src的属性<img>)。

静态网页数据爬取

厦门市生态环境局行政执法栏目为例说明静态网页爬取过程。

爬取网页数据,首先需要确定网页数据的位置。通常可以采用CSS selector he和Xpath。CSS selector通过数据所在对象的样式和模式指定位置,参见CSS Selector的语法;而XPath使用路径表达式来选取网页文档中的节点,可以参考XPath的语法。可以通过Chrome浏览器的开发者检查元素功能,找到数据所在的页面的对象,帮助获取样式或模式信息,确定对象的位置。示例网站处罚书来自div的标签对象,其属性class类别名为gl_list1,并且依次向下有ul、li和a三个层级,a链接对象中是处罚书的名称等内容。通过rvest包中的read_html()、html_nodes()、html_text()、html_attr()函数可以实现获取html文本、网页对象节点、网页对象内容、网页对象属性内容等功能。

library(rvest)


myurl = "https://sthjj.xm.gov.cn/zwgk/gsgk/xzcf/"

#获取html网页文本
web<-read_html(myurl, encoding="UTF-8") 

# 获取网页数据处罚书
punishcompany <-  web |> html_nodes("div.gl_list1 ul li a") |> html_text()  
# 获取处罚时间
punishdate <-  web |> html_nodes("div.gl_list1 ul li span") |> html_text()  
# 获取处罚书链接
punishlink <- web |> html_nodes("div.gl_list1 ul li a") |> html_attr(name = "href")  
# 将数据保存到数据框
punish <- data.frame(company=punishcompany, date=punishdate, link=punishlink) 

可以通过循环,快速爬取多个类似结构的静态页面,循环从1到10,对应第2页到第11页的信息。

for(i in 1:10){
  url <- paste(myurl, "index_", i, ".htm", sep = "")
  web<-read_html(url,encoding="UTF-8")
  punishcompany <-  web |> html_nodes("div.gl_list1 ul li a") |> html_text() 
  punishdate <-  web |> html_nodes("div.gl_list1 ul li span") |> html_text() 
  punishlink <- web |> html_nodes("div.gl_list1 ul li a") |> html_attr(name = "href") 

  punish1 <- data.frame(company = punishcompany, date = punishdate, link = punishlink)
  punish <- punish |> rbind(punish1)
}

head(punish)  # 展示获取的处罚书数据
                                     company       date
1 厦门市腾盛兴电子技术有限公司行政处罚决定书 2024-11-26
2 厦门市玖玖机动车检测有限公司行政处罚决定书 2024-11-13
3 解除扣押决定书厦门市腾盛兴电子技术有限公司 2024-10-16
4                       张帅涛行政处罚决定书 2024-10-11
5         厦门日上钢圈有限公司行政处罚决定书 2024-10-11
6   厦门钟利机动车检测有限公司行政处罚决定书 2024-09-29
                            link
1 ./202411/t20241126_2903149.htm
2 ./202411/t20241113_2900716.htm
3 ./202411/t20241129_2903969.htm
4 ./202410/t20241011_2894766.htm
5 ./202410/t20241011_2894762.htm
6 ./202409/t20240929_2892737.htm
tail(punish)  # 确认是否已完整获取数据
                                                                       company
193 厦门文仪电脑材料有限公司责令改正违法行为决定书 闽厦(执法)环改〔2022〕1号
194                                责令改正违法行为决定书 厦门日上金属有限公司
195               厦门恒兴兴业机械有限公司行政处罚决定书 闽厦环罚〔2021〕363号
196                  厦门海湾化工有限公司行政处罚决定书  闽厦环罚〔2021〕345号
197                   厦门海湾化工有限公司行政处罚决定书 闽厦环罚〔2021〕344号
198  厦门三荣陶瓷开发有限公司不予行政处罚决定书  厦环(执法)不罚决字[2021]1号
          date                           link
193 2022-01-10 ./202201/t20220110_2616014.htm
194 2022-01-05 ./202305/t20230526_2761318.htm
195 2021-12-31 ./202201/t20220110_2616044.htm
196 2021-12-15 ./202112/t20211215_2608718.htm
197 2021-12-15 ./202112/t20211215_2608715.htm
198 2021-12-10 ./202112/t20211215_2608668.htm

动态页面的爬取

动态页面与静态页面不同,一般通过与用户建立对话(session)获取用户的数据请求,采用Javascript等程序语言建立定制页面返回数据。因此采用静态页面的方法是无法定位数据的。下面以厦门市生态环境局的咨询投诉栏目为例,介绍动态页面的爬取。

myurl = "https://sthjj.xm.gov.cn/gzcy/wyzx/"

web<-read_html(myurl, encoding="UTF-8")

web |> html_nodes("div.gl_list2 ul li a") 
{xml_nodeset (1)}
[1] <a ms-attr-href="'./index_18763.htm?id='+el.letterId+'&amp;chnlId='+el.ch ...
web |> html_nodes("div.gl_list2 ul li a") |> html_text()
[1] ""

动态页面可以先通过会话请求页面,在爬取数据后,可以模拟点击下一页,此时会话会指向下一页,便可以爬取下一页的数据,依此直到爬取结束。

library(chromote)

# 请求动态页面
myurl = "https://sthjj.xm.gov.cn/gzcy/wyzx/"
sess <- read_html_live(myurl) 

#sess$view()

consultdata <- NULL
i=1
# 爬取前10页咨询投诉信息
while(i<=10){
  consult <-  sess |> html_elements("div.gl_list2 ul li a") |> html_text()   # 获取相关数据
  date <-  sess |> html_elements("div.gl_list2 ul li font") |> html_text() 
  link <- sess |> html_elements("div.gl_list2 ul li a") |> html_attr(name = "href") 
  if(length(consult)==0| !(length(consult)==length(date)&length(consult)==length(link))) next 
  consulttemp <- data.frame(consult=consult, date=date, link=link)
  consultdata <- consultdata %>% rbind(consulttemp)     # 保存到数据框
  
  if(nrow(consultdata) == 200) break       # 如果完成爬取,退出
  sess$click("a.next.b-free-read-leaf")     # 模拟页面点击下一页
  i=i+1
}

# 检查数据
head(consultdata)  
tail(consultdata) 

2 清理数据

数据清理是数据分析中最耗时的任务。以下列出了最重要的步骤。虽然方法有很多,但使用dplyr和tidyr包是最快捷、最容易学习的方法之一。

函数 用途
dplyr select 选择变量/列
dplyr filter 选择观察值/行
dplyr mutate 转换或重新编码变量
dplyr summarize 汇总数据
dplyr group_by 识别需要进一步处理的子组
tidyr gather 将宽格式数据集转换为长格式
tidyr spread 将长格式数据集转换为宽格式

2.1 选择变量

该select函数从数据集选取指定的变量或列。

library(haven)

# 导入SPSS数据
cgss <- read_sav("cgss2015subset.sav")

library(dplyr)

# 从数据框cgss中选择变量id,sex和age构成新数据集
newdata <- select(cgss, id, sex, age)

# 选择变量id以及hp1与hp4之间的所有变量 
newdata <- select(cgss, id, hp1:hp4)

# 选择除了edu和lnincome以外的所有变量
newdata <- select(cgss, -edu, -lnincome)

2.2 选择观测

该filter函数选择符合特定条件的观测(行)。可以使用&(AND) 和|(OR) 逻辑符号组合多个条件。

library(dplyr)

# 为了方便展示,将带有值标签的变量转换为因子(使用值标签作为取值)   
cgss <- cgss %>% mutate(across(where(is.labelled), as_factor))

# 选择女性观测
newdata <- filter(cgss, sex == "女")

# 选择来自山东的女性
newdata <- filter(cgss, sex == "女" & province == "山东省")

# 选择来自东北三省的观测
newdata <- filter(cgss, 
                  province == "辽宁省" | 
                  province == "吉林省" | 
                  province == "黑龙江省")

# 更简洁的代码
newdata <- filter(cgss, 
                  province %in% 
                    c("辽宁省", "吉林省", "黑龙江省"))

2.3 变量创建与重新编码

mutate函数可以创建新变量或转换现有变量。

library(dplyr)

# 将身高从厘米转化成英寸,将体重从斤转化成磅 
newdata <- mutate(cgss, 
                  height = height * 0.394,
                  weight   = (weight/2) * 2.205)

# 如果收入高于8万元,则变量incomecat取值为"高",否则取值为"低"

newdata <- mutate(cgss, 
                  incomecat = ifelse(income > 80000, 
                                     "高", 
                                     "低"))
                  
# 将教育程度不是初中、普通高中、中专的转化为其他类别
newdata <- mutate(cgss, 
                  edu = ifelse(edu %in% 
                                     c("初中", "普通高中", "中专"),
                                     edu,
                                     "其他"))
                  
# 将身高大于200或小于75设定为缺失值
newdata <- mutate(cgss, 
                  height = ifelse(height < 75 | height > 200,
                                     NA,
                                     height))

2.4 汇总数据

summarize函数可用于描述性统计的数据汇总。可与by_group函数结合使用,用于按组计算统计值。na.rm=TRUE选项用于在计算平均值之前删除缺失值。

library(dplyr)

# 计算身高和体重的平均值
newdata <- summarize(cgss, 
                     mean_ht = mean(height, na.rm=TRUE), 
                     mean_weight = mean(weight, na.rm=TRUE))
newdata
# A tibble: 1 × 2
  mean_ht mean_weight
    <dbl>       <dbl>
1    164.        122.
# 分性别组计算身高和体重的平均值
newdata <- group_by(cgss, sex)
newdata <- summarize(newdata, 
                     mean_ht = mean(height, na.rm=TRUE), 
                     mean_wt = mean(weight, na.rm=TRUE))
newdata
# A tibble: 2 × 3
  sex   mean_ht mean_wt
  <fct>   <dbl>   <dbl>
1 男       170.    133.
2 女       159.    113.

2.5 使用管道

dplyr和tidyr软件包允许使用管道运算符以紧凑的格式编写代码%>%,运算符%>%将左边的结果传递给右边的函数的第一个参数。例如:

library(dplyr)

newdata <- filter(cgss, 
                  sex == "女")
newdata <- group_by(newdata, province)
newdata <- summarize(newdata, 
                     mean_ht = mean(height, na.rm = TRUE))

# 采用管道操作符更简洁,也更符合人类思维
newdata <- cgss %>%
  filter(sex == "女") %>%
  group_by(province) %>%
  summarize(mean_ht = mean(height, na.rm = TRUE))

2.6 日期数据的处理

在 R 中,日期值以字符的形式输入。例如,记录 3 个人出生日期的简单数据集。

df <- data.frame(
  dob = c("11/10/1963", "Jan-23-91", "12:1:2001")
)

str(df) 
'data.frame':   3 obs. of  1 variable:
 $ dob: chr  "11/10/1963" "Jan-23-91" "12:1:2001"

将字符变量转换为日期变量的方法有很多。最简单的方法之一是使用lubridate包中提供的函数。这些函数包括ymd、dmy和 ,mdy分别用于导入年-月-日、日-月-年和月-日-年的格式。

library(lubridate)
# 将dob变量值从字符转化为日期型数据
df$dob <- mdy(df$dob)
str(df)
'data.frame':   3 obs. of  1 variable:
 $ dob: Date, format: "1963-11-10" "1991-01-23" ...

这些值在R的内部记录为自 1970 年 1 月 1日以来的天数,可以方便地执行日期运算,提取日期元素(月、日、年),重新格式化(例如,1963年10月11日)。 日期变量对于制作时间相关图表非常重要。

2.7 重塑数据

有些图表要求数据为宽格式,而有些图表则要求数据为长格式,示例如下。

将宽数据集转换为长数据集

library(tidyr)
wide_data <- data.frame(id = c("01", "02", "03"),
                        name = c("张三", "李四", "王五"),
                        sex = c("男", "男", "女"),
                        height = c(70, 72, 62),
                        weight = c(180, 195, 130))
knitr::kable(wide_data, caption = "宽数据")
宽数据
id name sex height weight
01 张三 70 180
02 李四 72 195
03 王五 62 130
long_data <- pivot_longer(wide_data,
                          cols = c("height", "weight"),
                          names_to = "variable", 
                          values_to ="value")

将长数据转化为宽数据

library(tidyr)
knitr::kable(long_data, caption = "长数据")
长数据
id name sex variable value
01 张三 height 70
01 张三 weight 180
02 李四 height 72
02 李四 weight 195
03 王五 height 62
03 王五 weight 130
wide_data <- pivot_wider(long_data,
                         names_from = "variable",
                         values_from = "value")

2.8 缺失数据

真实数据很可能包含缺失值。处理缺失数据有三种基本方法:特征选择、列删除和插补。ggplot2包中的msleep数据集描述了哺乳动物的睡眠习惯,并且在多个变量上存在缺失值。

  • 特征选择 在特征选择中,可以删除包含太多缺失值的变量(列)。
data(msleep, package="ggplot2")

# 每个变量中缺失值的比例
pctmiss <- colSums(is.na(msleep))/nrow(msleep)
round(pctmiss, 2)
        name        genus         vore        order conservation  sleep_total 
        0.00         0.00         0.08         0.00         0.35         0.00 
   sleep_rem  sleep_cycle        awake      brainwt       bodywt 
        0.27         0.61         0.00         0.33         0.00 

61% 的 sleep_cycle 值缺失。可以决定将其删除。

  • 按列删除
    整行删除包含缺失值的观测。
newdata <- select(msleep, genus, vore, conservation)
newdata <- na.omit(newdata)
  • 补值
    插补涉及用“合理”的猜测值(假设缺失值不存在时的值)来替换缺失值。有几种方法,详见VIM、mice、Amelia和missForest等包。这里将使用VIMkNN()包中的函数,用插补值替换缺失值。
# 用5个最近邻的值插补缺失值
library(VIM)
newdata <- kNN(msleep, k=5)

基本上,对于每个有缺失值的案例,都会选择k个最相似的、没有缺失值的案例。如果缺失值为数值型,则使用这k 个案例的中位数作为插补值。如果缺失值为类别值,则使用这k 个案例中出现频率最高的值。该过程会迭代所有观测和变量,直到结果收敛(趋于稳定)。

重要提示:缺失值可能会对研究结果造成偏差(有时甚至非常严重)。如果有大量缺失数据,在删除观测或填补缺失值之前,要慎重考虑其合理性。

参考书籍

  • Hadley Wickham, Mine Cetinkaya-Rundel, Garrett Grolemund. R for Data Science: Import, Tidy, Transform, Visualize, and Model Data, (2nd edition),O’Reilly Media, 2023.
  • Kabacoff (王小宁等译) R语言实战(第3版),人民邮电出版社,2023.
  • Ulrich Matter Big Data Analytics: A Guide to Data Science Practitioners Making the Transition to Big Data, CRC Press, 2024.