R语言基础

Published

September 8, 2025

1 基本设置

1.1 安装和使用包

install.packages("foreign")
library(foreign)

1.2 设置工作目录

R 有一个工作目录概念,会在工作目录下查找你要求它加载的文件,也会将你要求它保存的文件存放在这里。RStudio 会在控制台顶部显示你当前的工作目录。

如果只是单个简单代码脚本,可以在 R 内部设置工作目录。如果有多个脚本和各种格式的文件,建议建立项目文件。

setwd("~/Downloads/rproject/presention")
getwd()

1.3 脚本和项目

尽可能的用脚本记录所有的操作,即从原始数据到最终的结果。避免保存中间结果,或者将环境中的变量保存下来,而是用脚本去还原。

RStudio 会在退出时自动保存脚本编辑器的内容,并在重新打开时自动重新加载。建议在退出前将脚本保存在合适的文件夹,避免使用“Untitled1”、“Untitled2”、“Untitled3”之类的名称,而是保存为可理解的文件名。

文件名命名原则如下:

  • 文件名应易于机器识别:避免使用空格、符号和特殊字符。不要依赖大小写来区分文件。
  • 文件名应该是人类可读的:使用文件名来描述文件中的内容。
  • 文件名应该与默认运行顺序一致:文件名以数字开头,以便按字母顺序排列使用顺序。
01-load-data.R
02-exploratory-analysis.R
03-model-approach-1.R
04-model-approach-2.R
fig-01.png
fig-02.png
report-2022-03-20.qmd
report-2022-04-02.qmd
report-draft-notes.txt

将与给定项目相关的所有文件(输入数据、R 脚本、分析结果和图表)放在一个目录中更为方便。RStudio 通过项目文件进行支持。单击“文件”>“新建项目”,然后按照提示步骤操作。

新建项目后,在脚本编辑器中输入命令保存文件,运行完整的脚本,RStudio会自动将新生成的 文件保存到项目文件夹。关闭编辑器后,可以通过项目文件夹中.Rproj文件重新打开项目,继续上次的工作。

2 基本运算

2 + 2 ## 加
[1] 4
9 - 4 ## 减
[1] 5
6 * 12 ## 乘
[1] 72
86 / 4 ## 除
[1] 21.5
2 ^ 8 ## 次方
[1] 256
log(12) ## 自然对数  
[1] 2.484907
exp(2) ## e的指数
[1] 7.389056
2 / 9 ^ 2 == 2 / 81 
[1] TRUE
2 / 9 ^ 2 == (2/9) ^ 2
[1] FALSE

3 创建数据对象

3.1 单个元素的对象(整数、小数、字符串、逻辑符号)

integer_obj <- 4 ## 整数
numeric_obj <- 4.39053 ## 实数
string_obj <- "This is a string" ## 字符串 
logical_obj <- TRUE ## 逻辑符号 

3.2 多个元素组成的对象

向量

vector_of_integers <- c(3, 9, 10, 4) 
vector_of_integers
[1]  3  9 10  4
vector_of_numerics <- rep(4.39024, 10) # rep函数赋值
vector_of_numerics
 [1] 4.39024 4.39024 4.39024 4.39024 4.39024 4.39024 4.39024 4.39024 4.39024
[10] 4.39024
sequentialvec <- 1:10 #冒号赋值
backwardssequence <- 10:1
seqvec2 <- seq(0, 50, 10) #seq函数赋值
sequentialvec  
 [1]  1  2  3  4  5  6  7  8  9 10
backwardssequence
 [1] 10  9  8  7  6  5  4  3  2  1
seqvec2
[1]  0 10 20 30 40 50

向量的运算

vector_of_integers - 2 * vector_of_integers 
[1]  -3  -9 -10  -4
vector_of_integers
[1]  3  9 10  4

向量中元素的选择

vector_of_integers
[1]  3  9 10  4
vector_of_integers[1] 
[1] 3
vector_of_integers[2:4] 
[1]  9 10  4
# 求平均值
mean_of_vector_of_integers <- mean(vector_of_integers)
mean_of_vector_of_integers
[1] 6.5

练习:创建一个包含100到200之间所有整数的向量seq12,求其平均值,并赋值给mean12

seq12 <- seq(100,200,1)
mean12 <- mean(seq12)
mean12
[1] 150

矩阵

  • 采用向量创建矩阵
vector_of_integers
[1]  3  9 10  4
two_by_two_matrix <- matrix(vector_of_integers, nrow=2, ncol=2) 
two_by_two_matrix
     [,1] [,2]
[1,]    3   10
[2,]    9    4
  • 选择特定的矩阵元素
two_by_two_matrix[1,2] 
[1] 10
two_by_two_matrix[,2] 
[1] 10  4
two_by_two_matrix[,1]
[1] 3 9
  • 矩阵加法、矩阵按元素相乘、矩阵乘法
two_by_two_matrix + two_by_two_matrix 
     [,1] [,2]
[1,]    6   20
[2,]   18    8
two_by_two_matrix * two_by_two_matrix
     [,1] [,2]
[1,]    9  100
[2,]   81   16
two_by_two_matrix %*% two_by_two_matrix 
     [,1] [,2]
[1,]   99   70
[2,]   63  106

4 布尔运算

2 == 2 ## 相等,注意 == 代表布尔运算,= 代表赋值
[1] TRUE
2 != 2 ## 不相等
[1] FALSE
4 > 2 ## 大于
[1] TRUE
4 >= 2 ## 大于或等于
[1] TRUE
2 < 4 ## 小于
[1] TRUE
2 <= 4 ## 小于或等于
[1] TRUE
!(4 > 2) # 返回 FALSE
[1] FALSE
2==2 & 4==2 ## 采用&和|来连接布尔运算 
[1] FALSE
2==2 | 4==2 
[1] TRUE
c(3,5,2,0,-4,-5) > 0
[1]  TRUE  TRUE  TRUE FALSE FALSE FALSE
full <- state.name ## 系统存储的美国各州名单
full[1:10]
 [1] "Alabama"     "Alaska"      "Arizona"     "Arkansas"    "California" 
 [6] "Colorado"    "Connecticut" "Delaware"    "Florida"     "Georgia"    
pacstates <- c("Washington", "Oregon", "California", "Alaska", "Hawaii")
!(full %in% pacstates)[1:10]
 [1]  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE
full[!(full %in% pacstates)]
 [1] "Alabama"        "Arizona"        "Arkansas"       "Colorado"      
 [5] "Connecticut"    "Delaware"       "Florida"        "Georgia"       
 [9] "Idaho"          "Illinois"       "Indiana"        "Iowa"          
[13] "Kansas"         "Kentucky"       "Louisiana"      "Maine"         
[17] "Maryland"       "Massachusetts"  "Michigan"       "Minnesota"     
[21] "Mississippi"    "Missouri"       "Montana"        "Nebraska"      
[25] "Nevada"         "New Hampshire"  "New Jersey"     "New Mexico"    
[29] "New York"       "North Carolina" "North Dakota"   "Ohio"          
[33] "Oklahoma"       "Pennsylvania"   "Rhode Island"   "South Carolina"
[37] "South Dakota"   "Tennessee"      "Texas"          "Utah"          
[41] "Vermont"        "Virginia"       "West Virginia"  "Wisconsin"     
[45] "Wyoming"       

5 函数

mean(c(2,3,10,3,NA), na.rm = TRUE) 
[1] 4.5
mean(x = c(2,3,10,3)) 
[1] 4.5

自定义函数

add_2 <- function(x){ 
k <- x + 2 # 加 2
return(k) # 返回 k 
}
adding_2_to_6 <- add_2(6)
adding_2_to_6
[1] 8

练习:编写函数对向量元素进行开3次幂并除以自然对数的计算,并返回值

example <- function(x){
  return(x^(1/3) / log(x)) 
  }
example(5:10)
[1] 1.0624678 1.0141543 0.9830522 0.9617967 0.9466869 0.9356591

6 条件执行

x <- 1000000
if(x > 10000){
  "x非常大"
}
[1] "x非常大"
if(x < 10000){
  "x并不大"
}

x <- 1
if(x > 10000){
  "x非常大"
}  else{
  "x并不大"
}
[1] "x并不大"
ifelse(x > 10000,
  "x非常大",
  "x并不大")
[1] "x并不大"
y <- "数学!"
if(!is.numeric(y)) {
  "y 不是数字格式"
} else if(y == 10000){
  "y等于10000"
} else if(y > 10000){
  "y非常大"
}  else{
  "y并不大"
}
[1] "y 不是数字格式"
ifelse(!is.numeric(y),
  "y 不是数字格式",
ifelse(y == 10000,
  "y 等于10000",
ifelse(y > 10000,
  "y 非常大",
  "y 并不大")))
[1] "y 不是数字格式"

7 重复与循环

for(i in 1:5){
  print(i)
  }
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
for(i in seq(1,5)){
  print(i)
  }
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
index <- c("look", "how", "fancy", "we", "can",
"be", "with", "loop", "indexes")
for(i in index){
  print(i)
  }
[1] "look"
[1] "how"
[1] "fancy"
[1] "we"
[1] "can"
[1] "be"
[1] "with"
[1] "loop"
[1] "indexes"
for(i in 1:10){
  for(j in 50:60){
    ## some sort of code...
    }
  }

8 数据框

8.1 读入数据生成数据框

houses_data <- read.table(file = "houses.txt", header = T, sep = "\t")

8.2 数据框的基础操作

head(houses_data)
  PRICE SQFT AGE FEATS NE CUST COR  TAX
1  2050 2650  13     7  1    1   0 1639
2  2080 2600   *     4  1    1   0 1088
3  2150 2664   6     5  1    1   0 1193
4  2150 2921   3     6  1    1   0 1635
5  1999 2580   4     4  1    1   0 1732
6  1900 2580   4     4  1    0   0 1534
houses_data$PRICE[1:20]
 [1] 2050 2080 2150 2150 1999 1900 1800 1560 1450 1449 1375 1270 1250 1235 1170
[16] 1180 1155 1110 1139  995
houses_data[houses_data$SQFT > 2500,][1:5,]
  PRICE SQFT AGE FEATS NE CUST COR  TAX
1  2050 2650  13     7  1    1   0 1639
2  2080 2600   *     4  1    1   0 1088
3  2150 2664   6     5  1    1   0 1193
4  2150 2921   3     6  1    1   0 1635
5  1999 2580   4     4  1    1   0 1732
names(houses_data)
[1] "PRICE" "SQFT"  "AGE"   "FEATS" "NE"    "CUST"  "COR"   "TAX"  
houses_data[1:10,1]
 [1] 2050 2080 2150 2150 1999 1900 1800 1560 1450 1449
sq.ft.price <- NULL # 创建一个对象
for(i in 1:10000){
    sample.data <- houses_data[sample(1:nrow(houses_data), 100),]
    price.persqft <- sample.data$PRICE / sample.data$SQFT # 建立代表单位面积价格的变量
    sq.ft.price[i] <- mean(price.persqft) # 将每个样本均值存入向量
}

hist(sq.ft.price)

9 绘图

  • 直方图
hist(c(1,1,2,2,3,3,3,3), main = "直方图示例", xlab = "变量") 

  • 散点图
plot(x = c(1,2,3,4,5), y = c(4,2,0,1,4),  col = "Red", xlab ="x",ylab = "y",
     pch = 18) ## 点的形状 

10 获取帮助

10.1 R 语言自带帮助系统

??mean
?mean

10.2 AI工具

把想要实现的操作描述清楚或者直接把控制台反馈的错误信息贴入deepseek,一般能给出比较好的解决方案。此外,Rstudio的插件Copilot也能提供代码自动补齐和自动编写等功能。

10.3 网上搜索

  1. Stack Overflow http://stackoverflow.com/questions/tagged/r

  2. 拷贝报错信息,Google,一般都能查到解决问题的办法。

11 代码风格

11.1 命名

对象名称必须以字母开头,并且只能包含字母、数字、_ 和.。对象名称应具有描述性,可以采用多词命名规范,推荐使用snake_case 格式,即用_ 分隔小写单词。在编写代码时,简短名称(a1,b2等)节省的时间相对较少(尤其是自动完成功能可以帮助完成输入),但调试较长的代码时,会难以理解和回忆起缩写代表的变量,需要反复确认,可能会更耗时。

11.2 空格

除幂符号^外,数学运算符两边(即 +,-,*,/,==,<等)以及赋值运算符(<-)周围都应加上空格。

# 好的代码风格
z <- (a + b)^2 / d

# 不好的代码风格
z<-( a + b ) ^ 2/d

对于常规函数调用,不要在括号内外添加空格。逗号后始终添加空格,就像标准英语一样。

# 好的代码风格
mean(x, na.rm = TRUE)

# 不好的代码风格
mean (x ,na.rm=TRUE)

如果能提高对齐效果,方便浏览代码,添加额外的空格让代码看起来更齐整也是可以的。

flights |> 
  mutate(
    speed      = distance / air_time,
    dep_hour   = dep_time %/% 100,
    dep_minute = dep_time %%  100
  )

11.3 注释

# 号后的内容会被R语言看做是文本注释,不会运行。添加注释可以帮助其他人理解代码,也帮助自己以后调试理解代码。注释的内容可以重点解释为什么这么做(例如,为什么要更改默认的参数等)。

如果代码脚本很长,可以使用分段注释将文件分解为易于管理的部分。

# 数据获取 --------------------------------------

# 数据绘图 --------------------------------------

RStudio 提供了键盘快捷键来创建这些标题(Cmd/Ctrl + Shift + R),并将它们显示在编辑器左下方的代码导航下拉菜单中。在脚本中添加分段注释后,可以使用脚本编辑器左下角的代码导航工具轻松导航到它们。

12 数据导入

本部分介绍如何使用 readr 包加载数据文件,该包是核心 tidyverse 的一部分。

library(tidyverse)

12.1 从文件中读取数据

常见的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
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() 函数读入,参数是文件的路径。

students <- read_csv("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    

检查数据,发现需要处理缺失值、重新命名变量、定义分类变量、修正变量值等。

# 将字符字符串 N/A 识别为缺失值 NA
students <- read_csv("students.csv", na = c("N/A", ""))

# 对不符合命名规范的变量名进行重命名
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包能将不规范的命名改变成规范命名
students <- students |> janitor::clean_names()

# 将变量转化为因子
students <- students |>
  mutate(meal_plan = factor(meal_plan))

# 修正变量值
students <- students |>
  mutate(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

12.2 控制列类型

CSV文件不包含变量类型(例如逻辑型、数字型、字符串等)的信息,因此 readr 会尝试猜测类型。每一列抽取1000行的值,依次判断是否为逻辑性、数字型、日期型变量,如果都是不是,便为字符串。但是现实中,常常由于数据集不采用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 参数来测试,如果报错,则采用problems() 了解更多信息。

df <- read_csv(
  simple_csv, 
  col_types = list(x = col_double())
)
problems(df)
# A tibble: 1 × 5
    row   col expected actual file                                              
  <int> <int> <chr>    <chr>  <chr>                                             
1     3     1 a double .      /private/var/folders/3r/j3j4w0015hd4fxnyycp4nf6m0…

再设置新的缺失值 na = ".",得到符合要求的数字列。

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

readr 提供了九种列类型,包括 col_logical()col_double()col_integer()col_character()col_factor()col_date()col_datetime(),分别创建逻辑值、小数、整数、字符串、因子、日期和日期时间等列类型;col_number() 是宽松的数字解析器,会忽略非数字组件,可以表示货币,col_skip() 可以跳过某些数据列不读取。

12.3 从多个文件中读取数据

sales_files <- c("data/01-sales.csv", "data/02-sales.csv", "data/03-sales.csv")
# `id` 参数会在结果数据框中添加一个名为 `file` 的新列,用于标识数据来自哪个文件。
read_csv(sales_files, id = "file")

# 可以使用基础函数 `list.files()` 通过匹配文件名中的模式来查找文件,避免逐一写文件名
sales_files <- list.files("data", pattern = "sales\\.csv$", full.names = TRUE)
sales_files

12.4 写入文件

write_csv(students, "students.csv")

如果不希望列的数据结构信息消失,可以采用write_rds()read_rds() 替代。

write_rds(students, "students.rds")
read_rds("students.rds")

13 大数据编程

13.1 监测代码的表现

在 R 中编写用于处理大数据的数据分析脚本时,建议先用少量子样本测试脚本的关键代码。为了快速识别潜在的瓶颈,可以使用一些 R 包来精确跟踪脚本各部分的处理时间以及内存占用。

处理时间

# 获取循环代码运行的时间(每次依照计算机的进程会略有不同)
system.time(for (i in 1:100) {i + 5})
 用户  系统  流逝 
0.001 0.000 0.001 
library(microbenchmark)
# 获取代码运行时间的统计信息
microbenchmark(for (i in 1:100) {i + 5})
Unit: microseconds
                           expr     min       lq     mean  median       uq
 for (i in 1:100) {     i + 5 } 608.112 626.2955 698.4723 651.613 693.5765
      max neval
 4426.483   100

内存占用

hello <- "Hello, World!"
# 获取对象大小
object.size(hello)
120 bytes
# 创建字符串大向量
large_string <- rep(LETTERS[1:20], 1000^2)
head(large_string)
[1] "A" "B" "C" "D" "E" "F"
# 转换成因子
large_factor <- as.factor(large_string)

# 比较字符串对象与因子对象内存占用的大小
object.size(large_string) - object.size(large_factor)
79999456 bytes

快速检测代码瓶颈

library(profvis)

# 分析代码的表现
profvis({
        x <- 1:10000
        z <- 1.5

# 方法一:采用循环向量元素依次相乘
multiplication <- 
        function(x,z) {
                result <- c()
                for (i in 1:length(x)) {result <- c(result, x[i]*z)}
                return(result)
        }
result <- multiplication(x,z)

# 方法二:直接向量乘法
result2 <- x * z 
head(result2) 
})

运行后,会用可视化的方式显示每行代码占用内存和运行时间的信息。

13.2 预先分配足够内存

对象初始化时,会在内存中的某个位置默认占用少量内存。一旦对象增长,如果没有足够的空间,需要将其移动到空间更大的位置。这种重新分配内存的操作,需要时间,会减慢整个进程的速度。因此,更有效的代码应该预先分配足够的内存。

# 未预先分配足够内存
sqrt_vector <- 
     function(x) {
          output <- c()
          for (i in 1:length(x)) {
               output <- c(output, x[i]^(1/2))
          }
          
          return(output)
     }

# 预先分配足够内存
sqrt_vector_faster <- 
     function(x) {
# 根据参数x的长度,预先分配output占用的内存
          output <- rep(NA, length(x))
          for (i in 1:length(x)) {
               output[i] <-  x[i]^(1/2)
          }
          
          return(output)
     }

两种代码的运行时间对比如图。

13.3 使用向量化的R函数

许多 R 函数(例如数学运算符)都是向量化的,即操作直接作用于向量,从而利用向量中每个元素的相似性,从而可以增强运行效率。相对地,如果采用循环,R 必须在每次迭代中一遍又一遍地重复相同的准备步骤,会增加运行时间。

# 向量化的运行,不使用循环迭代
sqrt_vector_fastest <- 
     function(x) {
               output <-  x^(1/2)
          return(output)
     }

比较运行时间

即使没有可用的向量化 R 基础函数,也建议不要编写简单的循环,尽量使用 apply 型的函数来避免内存分配问题。

13.4 避免不必要的复制对象

R 脚本对于复制对象(b <- a)是将两个不同的名称(a和b)指向同一个存储空间(例如,一个非常大的文本向量)。但是,如果对某个名称的元素进行了改变(a[1] <- 0),那么R会复制一个存储空间来区分两个名称所指的对象,相当于占用空间扩大一倍。

a <- runif(10000)
b <- a

library(lobstr)
# 检查两个名称对象的地址
obj_addr(a)
[1] "0x122ef8000"
obj_addr(b)
[1] "0x122ef8000"
a[1] <- 0

obj_addr(a)
[1] "0x3100d8000"
obj_addr(b)
[1] "0x122ef8000"

13.5 及时释放内存

如果在程序运行过程中,不再需要某个较大的中间数据对象,可以通过 rm() 函数及时删除,释放紧张的内存。

library(pryr)
mem_change(large_vector <- runif(10^8))
800 MB
mem_change(rm(large_vector))
-800 MB

13.5 必要时使用低级语言

R 是一种解释型语言,运行代码时由解释器逐条将代码翻译成机器码运行。而低级的编译型语言是将代码整体编译成机器码,然后运行。运行已编译的代码比运行必须先逐条解释的代码快得多。事实上,R 的一些核心函数都是用低级编程语言实现的,调用的 R 函数只是与这些函数进行交互,例如 sum 函数等。必要时可以通过R语言调用低级语言编写的代码(例如data.table包),增加运行的速度。

参考书籍

  • 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.