library(readr)
# 导入逗号分割的数据
cgss2017 <- read_csv("cgss2017.csv")
# 导入制表符分割的数据
cgss2017 <- read_tsv("cgss2017.txt")数据获取与预处理
本部分介绍数据导入与获取、数据转化和清理。
1 数据导入与获取
R 几乎可以导入任何格式的数据,包括文本、Excel、SPSS和STATA等统计软件等格式的数据,同时R还能够连接数据库获取各种类型的数据。
1.1 文本文件
readr包提供了将分隔文本文件导入 R 数据框的函数选择。
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 & <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+'&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.