R 统计与机器学习研讨会(二)
原文:
annas-archive.org/md5/e61f21209b8b5ad343780cefdc55a102译者:飞龙
第三章:中间数据处理
上一章介绍了dplyr提供的用于数据处理的常用函数集。例如,在描述和提取数据集的统计信息时,我们可以使用group_by()和summarize()函数遵循拆分-应用-组合程序。本章从上一章继续,重点关注中间数据处理技术,包括转换分类和数值变量以及重塑数据框。除此之外,我们还将介绍用于处理文本数据的字符串操作技术,其格式与我们迄今为止所使用的整洁表格格式根本不同。
到本章结束时,你将能够执行更高级的数据操作,并将你的数据处理技能扩展到基于字符串的文本,这对于自然语言处理领域是基本的。
本章将涵盖以下主题:
-
转换分类和数值变量
-
重塑数据框
-
操作字符串数据
-
使用
stringr -
介绍正则表达式
-
使用整洁文本挖掘
技术要求
要完成本章的练习,你需要具备以下条件:
-
编写本文时,
rebus包的最新版本是 0.1-3 -
编写本文时,
tidytext包的最新版本是 0.3.2 -
编写本文时,
tm包的最新版本是 0.7-8
本章的所有代码和数据都可在以下链接找到:github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/tree/main/Chapter_3。
转换分类和数值变量
如前一章所述,我们可以使用dplyr中的mutate()函数来转换现有变量并创建新变量。具体的转换取决于变量的类型和我们希望其具有的形状。例如,我们可能希望根据映射字典更改分类变量的值,基于现有变量的过滤条件组合创建新变量,或者将数值变量分组到新的变量中的不同范围。让我们依次查看这些场景。
重新编码分类变量
在许多情况下,你可能需要重新编码变量的值,例如将国家的简称映射到相应的全称。让我们创建一个模拟的tibble数据集来展示这一点。
在以下代码中,我们创建了一个students变量,用于存储有关年龄、国家、性别和身高的信息。这是一个小型模拟数据集,但对于演示目的来说已经足够好了:
students = tibble(age = c(26, 30, 28, 31, 25, 29, 30, 29),
country = c('SG', 'CN', 'US', 'UK','CN', 'SG', 'IN', 'SG'),
gender = c('F', 'F', 'M', 'M', 'M', 'F', 'F', 'M'),
height = c(168, 169, 175, 178, 170, 170, 172, 180))
现在,让我们通过一个将country变量的值转换为全称的示例来了解这个过程。
练习 3.1 – 将国家变量值转换为全称
这个练习将使用 dplyr 包中的 recode() 函数将现有的短国家名称映射到相应的全称:
-
通过使用
recode()函数并提供映射表来添加一个新列,将短国家名称转换为相应的全称:students_new = students %>% mutate(country_fullname = recode(country, "SG"="Singapore", "CN"="China", "UK"="United Kingdom", "IN"="India")) >>> students_new # A tibble: 8 x 5 age country gender height country_fullname <dbl> <chr> <chr> <dbl> <chr> 1 26 SG F 168 Singapore 2 30 CN F 169 China 3 28 UK M 175 United Kingdom 4 31 UK M 178 United Kingdom 5 25 CN M 170 China 6 29 SG F 170 Singapore 7 30 IN F 172 India 8 29 SG M 180 Singapore在这里,我们提供了映射字典作为
recode()函数的参数,该函数在左侧列中搜索键,并将右侧列中对应的值分配给country_fullname。请注意,新创建的列假定是字符类型。 -
执行相同的转换,并将结果存储为
factor类型:students_new = students_new %>% mutate(country_fullname2 = recode_factor(country, "SG"="Singapore", "CN"="China", "UK"="United Kingdom", "IN"="India")) >>> students_new # A tibble: 8 x 6 age country gender height country_fullname country_fullname2 <dbl> <chr> <chr> <dbl> <chr> <fct> 1 26 SG F 168 Singapore Singapore 2 30 CN F 169 China China 3 28 UK M 175 United Kingdom United Kingdom 4 31 UK M 178 United Kingdom United Kingdom 5 25 CN M 170 China China 6 29 SG F 170 Singapore Singapore 7 30 IN F 172 India India 8 29 SG M 180 Singapore Singapore我们可以看到,使用
recode_factor()后,生成的变量country_fullname2是一个因子。
当我们想要创建的新列依赖于现有列的复杂组合时,我们可以求助于下一节中介绍的 case_when() 函数。
使用 case_when() 创建变量
case_when() 函数提供了一个在创建新变量时设置多个 if-else 条件的便捷方式。它接受一系列双向公式,其中左侧包含筛选条件,右侧提供与先前条件匹配的替换值。函数内部的语法遵循 逻辑条件(们) ~ 替换值 的模式,这些条件按顺序进行评估,其中可以在逻辑条件中使用多个变量。序列的末尾是一个 TRUE ~ 默认值 的情况,如果所有先前条件评估为 FALSE,则将该值分配给变量。
让我们通过一个练习来创建一个基于多个 if-else 条件的新变量,这些条件涉及多个列。
练习 3.2 – 使用多个条件和列创建新变量
在这个练习中,我们将创建一个新变量,该变量指示学生的年龄和地区。
创建一个新变量类型,用于识别学生是否来自亚洲,以及他们是否在 20 多岁或 30 多岁,分别假设 asia_20+ 和 asia_30+ 作为值。如果没有匹配项,将值设置为 others:
students_new = students %>%
mutate(type = case_when(age >= 30 & country %in% c("SG","IN","CN") ~ "asia_30+",
age < 30 & age >= 20 & country %in% c("SG","IN","CN") ~ "asia_20+",
TRUE ~ "others"))
>>> students_new
# A tibble: 8 x 5
age country gender height type
<dbl> <chr> <chr> <dbl> <chr>
1 26 SG F 168 asia_20+
2 30 CN F 169 asia_30+
3 28 UK M 175 others
4 31 UK M 178 others
5 25 CN M 170 asia_20+
6 29 SG F 170 asia_20+
7 30 IN F 172 asia_30+
8 29 SG M 180 asia_20+
在这里,我们使用了 & 符号来组合多个评估 年龄 和 国家 的 AND 条件。当序列中的前一个条件都不评估为 TRUE 时,函数将落入包含所有条件的 TRUE 情况,并将 others 作为默认值分配。
接下来,我们将查看将数值列转换为不同的箱/类别。
使用 cut() 对数值变量进行分箱
可以使用 cut() 函数将数值列划分为不同的类别。对于数值列,它将值分配给相应的预定义区间,并根据分配的区间对值进行编码。生成的区间列假定是一个有序因子类型。
cut()函数有三个关键参数:x用于接受要分组的数值向量,breaks用于接受一个数值向量作为切割点,这可能包括负无穷大-Inf和正无穷大Inf,以及labels用于指示结果区间的标签。
让我们通过使用cut()将age列转换为不同的年龄组进行一次练习。
练习 3.3 – 将年龄列分为三个组
在这个练习中,我们将使用cut()函数将age列的值分配到以下范围之一:(-infinity, 25)、[26, 30]或[31, infinity]:
-
将
age列分为三个区间,断点为25和30(右侧包含),并将它们存储在一个名为age_group的新列中:students_new = students %>% mutate(age_group = cut(x = age, breaks = c(-Inf, 25, 30, Inf), labels = c("<=25", "26-30", ">30"))) >>> students_new # A tibble: 8 x 5 age country gender height age_group <dbl> <chr> <chr> <dbl> <fct> 1 26 SG F 168 26-30 2 30 CN F 169 26-30 3 28 UK M 175 26-30 4 31 UK M 178 >30 5 25 CN M 170 <=25 6 29 SG F 170 26-30 7 30 IN F 172 26-30 8 29 SG M 180 26-30这里,我们可以看到
age_group是一个有序因子,有三个层级。几个切割函数在没有特定截止点的情况下执行自动分组。例如,
cut_interval()将原始向量切割成指定数量的等间隔组,而cut_number()将输入向量转换为指定数量的组,其中每个组大约有相同数量的观测值。tidyverse包提供了这两个函数。让我们尝试一下。 -
使用
cut_interval()将age列分为三个等长的组:students_new = students %>% mutate(age_group = cut_interval(age, n=3)) >>> students_new # A tibble: 8 x 5 age country gender height age_group <dbl> <chr> <chr> <dbl> <fct> 1 26 SG F 168 [25,27] 2 30 CN F 169 (29,31] 3 28 UK M 175 (27,29] 4 31 UK M 178 (29,31] 5 25 CN M 170 [25,27] 6 29 SG F 170 (27,29] 7 30 IN F 172 (29,31) 8 29 SG M 180 (27,29)age_group列现在由三个代表等长区间的层级组成。让我们使用summary()函数检查每个层级的计数:>>> summary(students_new$age_group) [25,27] (27,29) (29,31) 2 3 3 -
使用
cut_interval()将age列分为具有相等观测数的三个组:students_new = students %>% mutate(age_group = cut_number(age, n=3)) >>> students_new # A tibble: 8 x 5 age country gender height age_group <dbl> <chr> <chr> <dbl> <fct> 1 26 SG F 168 [25,28.3] 2 30 CN F 169 (29.7,31] 3 28 UK M 175 [25,28.3] 4 31 UK M 178 (29.7,31] 5 25 CN M 170 [25,28.3] 6 29 SG F 170 (28.3,29.7] 7 30 IN F 172 (29.7,31) 8 29 SG M 180 (28.3,29.7)截止点现在假设为小数点,以使观测值的计数大约相等,如下面的代码所验证:
>>> summary(students_new$age_group) [25,28.3] (28.3,29.7) (29.7,31) 3 2 3
到目前为止,我们已经探讨了不同的方法来转换现有的分类或数值变量,并基于特定条件创建新变量。接下来,我们将探讨如何转换和重塑整个 DataFrame,以方便我们的分析。
重塑 DataFrame
由分类和数值列组合而成的 DataFrame 可以用宽格式和长格式表示。例如,studentsDataFrame 被认为是长格式,因为所有国家都存储在country列中。根据处理的具体目的,我们可能希望为数据集中的每个唯一国家创建一个单独的列,这会增加 DataFrame 的列数,并将其转换为宽格式。
通过spread()和gather()函数可以在宽格式和长格式之间进行转换,这两个函数都由tidyr包提供,属于tidyverse生态系统。让我们看看它在实际中的应用。
使用spread()将长格式转换为宽格式
有时会需要将长格式 DataFrame 转换为宽格式。spread() 函数可以将具有多个类别的分类列转换为由 key 参数指定的多个列,每个类别作为 DataFrame 中的单独列添加。列名将是分类列的唯一值。value 参数指定在调用 spread() 函数时要在这些附加列中展开和填充的内容。让我们通过一个练习来了解。
练习 3.4 – 将长格式转换为宽格式
在这个练习中,我们将使用 spread() 将 students DataFrame 转换为宽格式:
-
使用
country作为key参数,height作为value参数,使用spread()将学生转换为宽格式。将结果 DataFrame 存储在students_wide中:students_wide = students %>% spread(key = country, value = height) >>> students_wide # A tibble: 7 x 6 age gender CN IN SG UK <dbl> <chr> <dbl> <dbl> <dbl> <dbl> 1 25 M 170 NA NA NA 2 26 F NA NA 168 NA 3 28 M NA NA NA 175 4 29 F NA NA 170 NA 5 29 M NA NA 180 NA 6 30 F 169 172 NA NA 7 31 M NA NA NA 178我们可以看到原始的
height列消失了,并增加了四个附加列。这四个列对应于唯一的各国,这些列的值由身高填充。如果特定国家的对应身高不可用,则使用NA填充缺失的组合。如果我们想为这些
NA值指定默认值,我们可以在spread()中设置fill参数。 -
使用四舍五入的平均身高来填充结果宽格式中的
NA值。将结果 DataFrame 存储在students_wide2中:avg_height = round(mean(students$height)) students_wide2 = students %>% spread(key = country, value = height, fill = avg_height) >>> students_wide2 # A tibble: 7 x 6 age gender CN IN SG UK <dbl> <chr> <dbl> <dbl> <dbl> <dbl> 1 25 M 170 173 173 173 2 26 F 173 173 168 173 3 28 M 173 173 173 175 4 29 F 173 173 170 173 5 29 M 173 173 180 173 6 30 F 169 172 173 173 7 31 M 173 173 173 178
从长格式转换为宽格式在分析和展示方面可能很有帮助,因为我们可以直观地比较特定年龄和性别组合下所有国家的身高。然而,这会带来额外的存储成本,如之前显示的多个 NA 值所示。
现在,让我们学习如何将宽格式 DataFrame 转换为长格式。
使用 gather() 函数将宽格式转换为长格式
当我们处于相反的情况,即给定的数据是宽格式时,我们可以使用 gather() 函数将其转换为长格式,以便进行更方便的后续处理。例如,通过将四个国家列压缩到 key 变量中,并将所有身高存储在 gather() 中指定的 value 变量下,我们可以继续使用我们之前介绍过的基于两列(而不是四列或更多)的常规分割-应用-组合处理。
gather() 函数也使用 key 和 value 参数来指定长格式表中结果的 key 和 value 列的名称。此外,我们还需要指定用于填充 key 列的列名和 value 列中的值。当需要指定许多相邻的列时,我们可以通过传递起始和结束列名来使用 : 运算符选择所有中间的列。让我们通过一个练习来了解。
练习 3.5 – 将宽格式转换为长格式
本练习将宽格式化的students_wide DataFrame 转换回其原始的长格式:
-
通过指定
key列为country和value列为height,并将CN、IN、SG和UK列的值分别用于填充key和value列,将students_wide转换为长格式:students_long = students_wide %>% gather(key = "country", value = "height", CN:UK) >>> students_long # A tibble: 28 x 4 age gender country height <dbl> <chr> <chr> <dbl> 1 25 M CN 170 2 26 F CN NA 3 28 M CN NA 4 29 F CN NA 5 29 M CN NA 6 30 F CN 169 7 31 M CN NA 8 25 M IN NA 9 26 F IN NA 10 28 M IN NA # … with 18 more rows我们可以看到,由于
students_wide中原本就存在缺失值,height列中添加了几行缺失值。让我们使用dplyr中的drop_na()函数来删除它们。 -
删除
height列中的NA值行:students_long = students_long %>% drop_na(height) >>> students_long # A tibble: 8 x 4 age gender country height <dbl> <chr> <chr> <dbl> 1 25 M CN 170 2 30 F CN 169 3 30 F IN 172 4 26 F SG 168 5 29 F SG 170 6 29 M SG 180 7 28 M UK 175 8 31 M UK 178通过这样,我们已经获得了长格式的 DataFrame。现在,让我们验证它是否与原始的
studentsDataFrame 相同。 -
使用
all_equal()验证students_long是否与students相同:>>> all_equal(students, students_long, ignore_row_order = T, ignore_col_order = T) TRUEdplyr中的all_equal()函数比较两个数据集并检查它们是否相同。它提供了一种灵活的方法来进行等价比较,并支持忽略行和/或列的顺序。结果显示,我们已经成功转换回原始数据集。
通过这样,我们已经探讨了不同的方法来重塑 DataFrame。接下来,我们将介绍如何处理字符串数据。
操作字符串数据
字符串类型在现实生活中的数据中很常见,例如姓名和地址。分析字符串数据需要正确清理原始字符,并将文本数据块中嵌入的信息转换为可量化的数值摘要。例如,我们可能想要找到所有遵循特定模式的学生的匹配姓名。
本节将介绍通过正则表达式定义不同模式的方法,以检测、分割和提取字符串数据。让我们从字符串的基础知识开始。
创建字符串
有时,单个引号(')也被用来表示字符串,尽管通常建议除非字符本身包含双引号,否则使用双引号。
创建字符串有多种方式。以下练习介绍了初始化字符类型字符串的几种不同方法。
练习 3.6 – 在 R 中表达字符串
在这个练习中,我们将探讨如何在 R 中创建字符串:
-
尝试在 R 控制台中键入以下字符串:
>>> "statistics workshop" "statistics workshop"字符串被正确打印出来。让我们看看如果我们用双引号包裹
statistics会发生什么。 -
在字符串中给
statistics添加双引号:>>> ""statistics" workshop" Error: unexpected symbol in """statistics"这次,出现了一个错误,因为 R 将第二个双引号视为字符串的结束引号。可以通过在字符串中使用双引号时,将外部引号切换为单引号来避免这个错误。
-
用单引号包裹前面的字符串:
>>> '"statistics" workshop' "\"statistics\" workshop"现在,R 正确解释了字符串,并将单引号对内的所有内容视为一个整体字符串。请注意,结果字符串在控制台中仍然用双引号打印。字符串内的两个双引号前面也有一个反斜杠(
\)。这被称为转义序列,用于指示双引号作为字符的原始解释,而不是字符串的开始。转义序列是在字符串中包含特殊字符的有用方式。我们也可以在字符串内部手动添加转义字符,以强制正确解释,这将打印出与之前相同的结果。
-
在字符串内部双引号之前添加转义序列:
>>> "\"statistics\" workshop" "\"statistics\" workshop"使用反斜杠打印字符串序列不方便阅读。为了美化输出,我们可以将确切字符串传递给
writeLines()函数。 -
使用
writeLines()打印相同的字符串:>>> writeLines("\"statistics\" workshop") "statistics" workshop
接下来,我们将探讨如何将数字转换为字符串以进行更好的解释。
将数字转换为字符串
如我们之前所学,数字可以通过as.character()函数转换为字符串。然而,直接读取和报告像123000这样的大数字会很不方便。我们通常会以更易读的方式表达,例如 123,000,或者以更简洁的方式,例如 1.23e+05,后者遵循科学表示法,其中 e+05 等于 105。此外,我们可能还想显示浮点数小数点后有限位数的数字。
所有这些都可以通过format()函数实现,这在将数字作为字符串转换和打印时遵循不同格式时非常有用。让我们看看在实践中是如何做到这一点的。
练习 3.7 – 使用format()将数字转换为字符串
这个练习将使用format()将数字转换为漂亮且易于阅读的字符串:
-
通过在
format()函数中指定big.mark参数,将逗号作为千位分隔符添加到123000:>>> format(123000, big.mark = ",") "123,000"注意,现在结果是带有逗号的字符类型字符串。
-
将
123000转换为科学格式:>>> format(123000, scientific = TRUE) "1.23e+05"使用科学格式是表示大数字的简洁方式。我们还可以通过指定显示的数字位数来缩短一个长的浮点数。
-
通过指定
digits参数,仅显示1.256的三个数字:>>> format(1.256, digits = 3) "1.26"结果被四舍五入并转换为字符串,显示指定数量的三个数字。我们也可以使用
round()函数达到相同的四舍五入效果。 -
将
1.256四舍五入到两位小数:>>> round(1.256, digits = 2) 1.26这次,结果仍然是数值型,因为
round()不涉及类型转换。
在下一节中,我们将探讨连接多个字符串。
连接字符串
当有多个字符串时,我们可以使用paste()将它们连接并合并成一个字符串。如果我们想在程序中打印长而定制的消息而不是手动输入,这变得很重要。
paste() 函数接受任意数量的字符串输入作为参数,并将它们组合成一个。让我们看看它是如何工作的。
练习 3.8 – 使用 paste() 组合字符串
在这个练习中,我们将探讨不同的方法来组合多个字符串输入:
-
将
statistics和workshop字符串连接起来生成statistics workshop:>>> paste("statistics", "workshop") "statistics workshop"在这里,我们可以看到两个字符串之间自动添加了一个空格。这是由
sep参数控制的,它指定了字符串之间的填充内容,并假定默认值为空格。我们可以选择通过传递一个分隔字符来覆盖默认行为。 -
移除中间的空格并生成
statisticsworkshop:>>> paste("statistics", "workshop", sep = "") "statisticsworkshop" Let’s see what happens when we connect a single string to a vector of strings. -
将
statistics和workshop向量与course连接:>>> paste(c("statistics", "workshop"), "course") "statistics course" "workshop course"结果显示
course被添加到向量的每个元素中。这是通过底层的回收操作完成的,其中course被回收以便可以与向量中的每个字符串组合。这类似于 Python 中的广播机制。我们也可以通过指定
collapse参数来移除向量结构并将所有元素组合成一个字符串。 -
将之前的输出压缩成一个由
+分隔的单个字符串:>>> paste(c("statistics", "workshop"), "course", collapse = " + ") "statistics course + workshop course"在将组合向量的所有组件插入并按指定参数分隔后,结果是单个折叠的字符串。
到目前为止,我们已经了解了处理字符串数据的基本知识。tidyverse 生态系统提供的 stringr 包提供了许多方便的函数,如果我们想要对字符串有更灵活的控制,这将在本节中介绍。
使用 stringr 处理字符串
stringr 包提供了一套连贯的函数,所有这些函数都以 str_ 开头,旨在使字符串处理尽可能容易。
让我们从 stringr 的基本函数开始,通过复制上一个练习中的相同结果。
stringr 的基础知识
stringr 包中的 str_c() 函数可以像 paste() 一样连接多个字符串,具有类似的功能。让我们看看它的实际应用。
练习 3.9 – 使用 paste() 组合字符串
在这个练习中,我们将使用 str_c() 重复 练习 3.8 中的相同操作:
-
在
statistics和workshop之间添加一个分隔空格来连接:>>> str_c("statistics", "workshop", sep = " ") "statistics workshop"我们可以使用
sep参数来指定字符串之间的分隔符。 -
将
statistics和workshop向量与course结合:>>> str_c(c("statistics", "workshop"), "course", sep = " ") "statistics course" "workshop course"相同的回收行为也出现在这里。
-
将前面的输出压缩成一个由
+分隔的单个字符串:>>> str_c(c("statistics", "workshop"), "course", sep = " ", collapse = " + ") "statistics course + workshop course"
有两个其他常见的 stringr 函数:str_length(),它返回字符串的长度,和 str_sub(),它从字符串中减去部分内容:
-
例如,我们可以得到向量中每个字符串的长度,如下面的代码片段所示。
>>> str_length(c("statistics", "workshop")) 10 8 -
或者,我们可以使用来自基础 R 的
nchar()函数来达到相同的结果,如下所示:>>> nchar(c("statistics", "workshop")) 10 8 -
我们还可以使用
str_sub()通过提供起始和结束索引来提取字符串的一部分:>>> str_sub(c("statistics", "workshop"), start = 1, end = 3) "sta" "wor"
提取字符串的一部分是查找字符串中模式的一种方式。在下一节中,我们将介绍一种比位置索引更高级的字符串匹配方法。
字符串中的模式匹配
在字符串中匹配模式是提取文本数据中的信息的一种常见方式。当找到匹配时,我们可以根据匹配拆分或替换字符串,添加如匹配次数等额外数据,或执行其他基于文本的分析。让我们通过几个练习来熟悉字符串匹配。
练习 3.10 – 在字符串中定位匹配
在这个练习中,我们将介绍三个在字符串中定位匹配时常用的函数,包括使用 str_detect() 检测匹配、使用 str_subset() 选择具有匹配的向量字符串,以及使用 str_count() 在字符串中计算匹配次数:
-
在包含
statistics和workshop的字符串向量中检测stat的出现:>>> str_detect(c("statistics", "workshop"), "stat") TRUE FALSEstr_detect()函数在输入字符串中查找指定的模式,并返回一个与输入向量长度相同的逻辑向量,其中TRUE表示匹配,否则为FALSE。 -
选择包含
stat的字符串子集:>>> str_subset(c("statistics", "workshop"), "stat") "statistics"str_subset()函数一次完成检测和选择。它将仅返回与指定模式匹配的字符串。 -
计算前一个向量中每个字符串中
t的出现次数:>>> str_count(c("statistics", "workshop"), "t") 3 0str_count()函数返回一个与输入向量长度相同的整数向量,显示每个字符串中特定匹配的频率。
接下来,我们将探讨如何根据特定的匹配来拆分字符串。
拆分字符串
根据特定模式拆分字符串可以通过 str_split() 函数实现,该函数假设与之前函数具有相似的命名和参数设置。然后原始字符串可以被分解成更小的部分以支持更精细的分析。让我们看看它是如何使用的。
练习 3.11 – 使用 str_split() 拆分字符串
这个练习将使用 str_split() 根据特定的匹配条件将字符串分解成更小的部分:
-
在
&符号处拆分statistics & machine learning workshop字符串:>>> str_split(c("statistics & machine leaning workshop"), "&") [[1]] [1] "statistics " " machine leaning workshop"结果是一个包含两个元素的向量列表,位于第一个条目中。请注意,结果子字符串中都有空格,这表明使用了精确的模式匹配来拆分字符串。然后我们可以将空格包含在匹配模式中,以移除结果子字符串中的空格。
-
在匹配模式中包含前导和尾随空格:
>>> str_split(c("statistics & machine leaning workshop"), " & ") [[1]] [1] "statistics" "machine leaning workshop"通过这种方式,子字符串中的空格已经被移除。如下面的代码片段所示,由于结果被包裹在一个列表中,我们可以遵循列表索引规则来访问相应的元素。
-
从上一个结果中访问第二个元素:
>>> str_split(c("statistics & machine leaning workshop"), " & ")[[1]][2] "machine leaning workshop" In this example, the first original string is split into two substrings while the second is split into three. Each original string corresponds to an entry in the list and can assume a different number of substrings. The resulting DataFrame will assume the same number of rows as the input vector and the same number of columns as the longest entry in the list.
接下来,我们将查看如何在字符串中替换匹配的模式。
替换字符串
str_replace() 和 str_replace_all() 函数使用 replacement 参数指定的新文本替换匹配项。区别在于 str_replace() 只替换第一个匹配项,而 str_replace_all() 如其名称所示替换所有匹配项。
让我们尝试使用两个函数将 & 符号替换为 and:
>>> str_replace(c("statistics & machine leaning workshop", "stats & ml & workshop"), pattern = "&", replacement = "and")
"statistics and machine leaning workshop" "stats and ml & workshop"
>>> str_replace_all(c("statistics & machine leaning workshop", "stats & ml & workshop"), pattern = "&", replacement = "and")
"statistics and machine leaning workshop" "stats and ml and workshop"
我们可以看到,第二个字符串中的所有 & 符号都被替换为 and。再次强调,替换特定匹配项涉及两个步骤:定位匹配项(如果有的话),然后执行替换。str_replace() 和 str_replace_all() 函数一次完成这两个步骤。
在下一节中,我们将遇到一个需要结合这些 stringr 函数的挑战。
整合起来
通常,一个特定的字符串处理任务会涉及使用多个 stringr 函数。这些函数结合在一起可以对文本数据进行有用的转换。让我们通过一个练习来整合到目前为止我们所学的知识。
练习 3.12 – 使用多个函数转换字符串
在这个练习中,我们将使用不同的基于字符串的函数将 statistics and machine leaning workshop 转换为 stats & ml workshop。首先,我们将 and 替换为 & 符号,分割字符串,并处理各个部分。让我们看看如何实现这一点:
-
创建一个
title变量来存储字符串,并将and替换为&:>>> title = "statistics and machine leaning workshop" >>> title = str_replace(title, pattern = "and", replacement = "&") >>> title "statistics & machine leaning workshop"在这里,我们使用
str_replace()将and替换为&。 -
使用
&将title分割成子字符串:>>> a = str_split(title, " & ") >>> a [[1]] [1] "statistics" "machine leaning workshop"在这里,我们使用
str_split()将原始字符串分割成更小的子字符串。注意,匹配模式中也添加了额外的空格。我们现在将处理这些单个部分。 -
将
statistics转换为stats:>>> b = str_c(str_sub(a[[1]][1], 1, 4), str_sub(a[[1]][1], -1, -1)) >>> b "stats"在这里,我们使用
str_sub()提取了前四个字符,即stat,以及最后一个字符s,然后使用str_c()将它们连接起来。注意-1表示字符串的最后一个位置索引。现在,我们可以开始处理第二部分。
-
使用空格分割
a变量的第二个元素:>>> c = unlist(str_split(a[[1]][2], " ")) >>> c "machine" "leaning" "workshop"在这里,我们使用
str_split()使用空格分割machine leaning workshop字符串,并使用unlist()将结果从列表转换为向量。我们这样做是为了在后续引用中节省一些打字,因为返回的列表中只有一个条目。现在,我们可以通过提取
machine和learning的第一个字符并将它们组合起来形成ml来重复类似的步骤。 -
根据前面的输出形成
ml:>>> d = str_c(str_sub(c[1], 1, 1), str_sub(c[2], 1, 1)) >>> d "ml"现在,我们可以将所有处理过的组件组合成一个字符串。
-
使用前面的输出形成最终的预期字符串:
>>> e = str_c(b, "&", d, c[3], sep = " ") >>> e "stats & ml workshop"
在下一节中,我们将学习更多使用正则表达式的先进模式匹配技术。
正则表达式介绍
一个rebus包。它是stringr的一个好伴侣,提供了便于字符串操作和使构建正则表达式更加容易的实用函数。记住,当你第一次使用它时,通过install.package("rebus")安装此包。
rebus包有一个特殊的操作符%R%,用于连接匹配条件。例如,为了检测一个字符串是否以特定的字符开始,比如s,我们可以指定模式为START %R% "s"并将其传递给str_detect()函数的模式参数,其中START是一个特殊关键字,用于指示字符串的开始。同样,END关键字表示字符串的结束。它们一起在rebus库中被称为锚点。让我们看看以下示例:
>>> str_detect(c("statistics", "machine learning"), pattern = START %R% "s")
TRUE FALSE
我们也可以在控制台中输入START。结果是箭头符号,这正是 vanilla 正则表达式中用来指示字符串开始的字符:
>>> START
<regex> ^
此外,str_view()是另一个有用的函数,它可视化字符串的匹配部分。运行以下命令将弹出一个带有高亮显示匹配部分的 HTML 查看器面板:
>>> str_view(c("statistics", "machine learning"), pattern = START %R% "s")
这在图 3.1中显示:
图 3.1 – 使用 str_view()在查看器面板中可视化匹配结果
让我们通过一个练习来了解 rebus 中各种模式匹配函数的更多内容。
练习 3.13 – 使用 rebus 应用正则表达式
在这个练习中,我们将应用不同的正则表达式来匹配字符串中的预期模式:
-
运行以下命令来创建一个字符串向量。注意,这些字符串设计得简单但足以展示我们将要介绍的匹配函数的目的:
>>> texts = c("stats 101", "machine learning", "R 101 ABC workshop", "101 R workshop") -
搜索以
learning结尾的向量中的字符串:>>> str_subset(texts, pattern = "learning" %R% END) "machine learning"这里,我们在
pattern参数中使用了END关键字来指示字符串应以learning结尾。 -
搜索包含任何字符后跟
101的字符串:>>> str_subset(texts, pattern = ANY_CHAR %R% "101") "stats 101" "R 101 ABC workshop"注意
ANY_CHAR是一个特殊关键字,是一个通配符,表示任何单个字符,在正常正则表达式中对应于点(.),如下面的代码所示:>>> ANY_CHAR <regex> .由于模式表示任何字符后跟
101,因此由于存在101,选出了两个字符串。101 R workshop没有被选中,因为没有字符在101之前。 -
搜索第三个字符为
a的字符串:>>> str_subset(texts, pattern = START %R% ANY_CHAR %R% ANY_CHAR %R% "a") "stats 101"这里,我们通过传递两个通配符关键字在开头来指定第三个字符为
a。 -
搜索以
stats或R开头的字符串:>>> str_subset(texts, pattern = START %R% or("stats", "R")) "stats 101" "R 101 ABC workshop"or()函数在指定多个匹配条件时很有用。 -
搜索包含一个或多个
a或A字符的字符串:>>> str_subset(texts, pattern = one_or_more(char_class("aA"))) "stats 101" "machine learning" "R 101 ABC workshop"在这里使用了两个新函数。
char_class()函数强制匹配输入参数中指定的允许字符之一,而one_or_more()函数表示括号内包含的模式可以重复一次或多次。
接下来,我们将介绍 tidytext 包,它允许我们方便地处理非结构化文本数据和 tidyverse 生态系统。
使用整洁文本进行挖掘
tidytext 包通过遵循整洁数据原则来处理非结构化文本,该原则规定数据应以结构化、矩形形状和类似 tibble 的对象表示。在文本挖掘的情况下,这需要将单个单元格中的文本转换为 DataFrame 中的每行一个标记。
对于一组文本(称为语料库)的另一种常用表示是文档-词矩阵,其中每一行代表一个文档(这可能是一句简短的句子或一篇长篇文章),每一列代表一个术语(整个语料库中唯一的单词,例如)。矩阵中的每个单元格通常包含一个代表性统计量,例如出现频率,以指示术语在文档中出现的次数。
在接下来的几节中,我们将深入了解这两种表示,并探讨如何将文档-词矩阵转换为整洁数据格式以进行文本挖掘。
使用 unnest_tokens() 将文本转换为整洁数据
让我们创建一个稍微不同的虚拟数据集,如下所示:
texts = c("stats 101", "Machine Learning", "R and ML workshop", "R workshop & Statistics with R")
texts_df = tibble(id = 1:length(texts), text = texts)
>>> texts_df
# A tibble: 4 x 2
id text
<int> <chr>
1 1 stats 101
2 2 Machine Learning
3 3 R and ML workshop
4 4 R workshop & Statistics with R
在这个数据集中,texts 列包含任意长度的文本。尽管它存储为 tibble 对象,但它并不非常适合整洁文本分析。例如,texts 列中的每一行都包含多个单词,这使得推导出诸如单词频率之类的统计总结变得具有挑战性。当每一行对应于所有文本的单个单词时,获取这些统计量会容易得多。
注意,在文本挖掘中查看单词级信息是常见的,尽管我们也可以扩展到其他变化,如单词对或甚至句子。用于文本挖掘的分析单位称为 tidytext 包中的 unnest_tokens() 函数。如果你还没有这样做,请记住安装并加载此包。
unnest_tokens() 函数接受两个输入:用于存储结果标记的列,以及将文本分解为标记的列。此外,unnest_tokens() 函数在将数据转换为整洁文本 DataFrame 时还处理其他方面。让我们通过一个练习来了解更多关于这个函数的信息。
练习 3.14 – 使用 unnest_tokens() 构建整洁文本
在这个练习中,我们将使用 unnest_tokens() 函数来构建整洁的文本并提取词频:
-
使用
unnest_tokens()将texts_df转换为整洁文本格式,并将包含标记的列命名为unit_token。将结果存储在tidy_df中:>>> tidy_df <- texts_df %>% unnest_tokens(unit_token, text) >>> tidy_df # A tibble: 13 x 2 id unit_token <int> <chr> 1 stats 2 1 101 3 2 machine 4 2 learning 5 3 r 6 3 and 7 3 ml 8 3 workshop 9 4 r 10 4 workshop 11 4 statistics 12 4 with 13 4 r注意,
unnest_tokens()默认使用单词级别的标记化;因此,unit_token列包含从相应文本中提取的所有单词标记,每个单词占一行。注意,由于unnest_tokens()默认移除所有标点符号并将所有单词转换为小写,&符号已从结果中移除。其余的列,如id,被保留并复制为原始文本字符串中的每个单词。我们还可以通过指定
token和 n 参数,使用双词(bigram)表示将texts_df转换为整洁数据:>>> tidy_df2 <- texts_df %>% unnest_tokens(unit_token, text, token = "ngrams", n = 2) >>> tidy_df2 # A tibble: 9 x 2 id unit_token <int> <chr> 1 1 stats 101 2 2 machine learning 3 3 r and 4 3 and ml 5 3 ml workshop 6 4 r workshop 7 4 workshop statistics 8 4 statistics with 9 4 with r我们可以看到,生成的标记由原始文本中的每个连续单词对组成。同样,在底层执行了标点符号的移除和转换为小写。
我们可以轻松地从可用的整洁数据中推导出单词频率分布。
-
从
tidy_df中推导单词计数:>>> tidy_df %>% count(unit_token, sort = TRUE) # A tibble: 10 x 2 unit_token n <chr> <int> 1 r 3 2 workshop 2 3 101 1 4 and 1 5 learning 1 6 machine 1 7 ml 1 8 statistics 1 9 stats 1 10 with 1在这里,我们使用了
count()函数来计算每个唯一单词的频率。我们还可以通过其他dplyr操作来叠加此分析,例如从单词计数中移除停用词(例如,the和a)。停用词是文本挖掘中不传达额外意义的常见单词,通常从语料库中移除。我们可以使用get_stopwords()函数检查英语停用词列表,如下所示:>>> get_stopwords() # A tibble: 175 x 2 word lexicon <chr> <chr> 1 i snowball 2 me snowball 3 my snowball 4 myself snowball 5 we snowball 6 our snowball 7 ours snowball 8 ourselves snowball 9 you snowball 10 your snowball # … with 165 more rows -
移除停用词后,推导单词频率。将结果存储在
tidy_df2中:>>> tidy_df2 = tidy_df %>% filter(!(unit_token %in% get_stopwords()$word)) %>% count(unit_token, sort = TRUE) >>> tidy_df2 # A tibble: 8 x 2 unit_token n <chr> <int> 1 r 3 2 workshop 2 3 101 1 4 learning 1 5 machine 1 6 ml 1 7 statistics 1 8 stats 1我们可以看到,结果中已经移除了
and和with。
接下来,我们将以文档-词矩阵的形式处理文本,这是在构建使用文本数据的机器学习模型时最常用的格式。
处理文档-词矩阵
我们可以将之前的整洁 DataFrame 转换为文档-词矩阵,也可以从文档-词矩阵转换回来。由于在前一个练习中我们使用了单词(unigram)表示,我们将继续使用单词频率,并查看如何在前面的练习中在整洁数据和文档-词矩阵之间进行转换。
常用的文本挖掘包是 tm。在继续进行以下练习之前,请记住安装并加载此包。
练习 3.15 – 转换为和从文档-词矩阵
在这个练习中,我们将以整洁格式获取单词频率表,然后将其转换为稀疏文档-词矩阵。稀疏矩阵是一种特殊的数据结构,它包含相同数量的信息,但比典型的 DataFrame 占用更少的内存空间。最后,我们将查看如何将文档-词矩阵转换回整洁格式:
-
使用前一个练习中的
tidy_df从每个文档和单词标记推导单词频率计数,并将结果保存到count_df中:>>> count_df = tidy_df %>% group_by(id, unit_token) %>% summarise(count=n()) >>> count_df # A tibble: 12 x 3 # Groups: id [4] id unit_token count <int> <chr> <int> 1 1 101 1 2 1 stats 1 3 2 learning 1 4 2 machine 1 5 3 and 1 6 3 ml 1 7 3 r 1 8 3 workshop 1 9 4 r 2 10 4 statistics 1 11 4 with 1 12 4 workshop 1在这里,第四个文档中
r出现了两次,其他所有单词都只出现一次。我们将将其转换为文档-词矩阵格式。 -
使用
tm包中的cast_dtm()函数将count_df转换为文档-词矩阵,并将结果存储在dtm中:>>> dtm = count_df %>% cast_dtm(id, unit_token, count) >>> dtm <<DocumentTermMatrix (documents: 4, terms: 10)>> Non-/sparse entries: 12/28 Sparsity : 70% Maximal term length: 10 Weighting : term frequency (tf)结果显示,我们总共有四篇文档和 10 个术语。稀疏度高达 70%,因为大多数单词只出现在各自的文档中。此外,表示文档中单词的统计量是词频。
我们还可以通过将其特别转换为普通矩阵来查看整个表格:
>>> as.data.frame(as.matrix(dtm), stringsAsFactors=False) 101 stats learning machine and ml r workshop statistics with 1 1 1 0 0 0 0 0 0 0 0 2 0 0 1 1 0 0 0 0 0 0 3 0 0 0 0 1 1 1 1 0 0 4 0 0 0 0 0 0 2 1 1 1现在,我们有了标准的文档-词矩阵。请注意,我们可以使用其他统计方法,例如
tf-idf,来表示矩阵中的每个单元格,或者甚至使用多个数值的向量来表示文档中的每个单词。后者被称为将dtm转换回整洁格式:>>> tidy_dtm = tidy(dtm) >>> tidy_dtm # A tibble: 12 x 3 document term count <chr> <chr> <dbl> 1 1 101 1 2 1 stats 1 3 2 learning 1 4 2 machine 1 5 3 and 1 6 3 ml 1 7 3 r 1 8 4 r 2 9 3 workshop 1 10 4 workshop 1 11 4 statistics 1 12 4 with 1现在,我们拥有与之前相同的数据整洁格式。
摘要
在本章中,我们讨论了几种中间数据处理技术,从结构化表格数据到非结构化文本数据。首先,我们介绍了如何转换分类和数值变量,包括使用recode()重新编码分类变量,使用case_when()创建新变量,以及使用cut()对数值变量进行分箱。接下来,我们探讨了如何重塑 DataFrame,包括使用spread()将长格式 DataFrame 转换为宽格式,以及使用gather()反向转换。我们还深入探讨了字符串的处理,包括如何创建、转换和格式化字符串数据。
此外,我们还介绍了有关stringr包的一些基本知识,该包提供了许多有用的实用函数,以简化字符串处理任务。常见的函数包括str_c()、str_sub()、str_subset()、str_detect()、str_split()、str_count()和str_replace()。这些函数可以组合起来创建一个强大且易于理解的字符串处理管道。
然后,我们介绍了使用rebus包的正则表达式,该包提供了与stringr配合良好的便利模式匹配功能。其函数和关键字易于阅读,包括START、END、ANY_CHAR、or()、one_or_more()等。
最后,我们介绍了使用tidytext包处理整洁文本数据。将一组文本数据转换为整洁格式,可以轻松利用tidyverse生态系统中的许多实用函数。unnest_tokens()函数通常用于整理原始文本,整洁的输出也可以转换为文档-词矩阵,这是开发机器学习模型的标准数据结构。
文本挖掘是一个很大的主题,我们在这章中只介绍了最基本的内容。希望这里展示的基本内容能够鼓励你进一步探索tidyverse生态系统提供的潜在功能。
在下一章中,我们将转换方向,介绍数据可视化,将我们处理过的数据转换为可视和可操作的见解。
第四章:使用 ggplot2 进行数据可视化
上一章介绍了中级数据处理技术,重点是处理字符串数据。当原始数据经过转换和处理,变成干净和结构化的形状后,我们可以通过在图表中可视化干净数据来将分析提升到下一个层次,这正是我们本章的目标。
到本章结束时,您将能够使用 ggplot2 软件包绘制标准图表,并添加自定义设置以呈现出色的视觉效果。
在本章中,我们将涵盖以下主题:
-
介绍
ggplot2 -
理解图形语法
-
图形中的几何形状
-
控制图形主题
技术要求
要完成本章的练习,您需要拥有以下软件包的最新版本:
-
ggplot2软件包,版本 3.3.6。或者,安装tidyverse软件包并直接加载ggplot2。 -
ggthemes软件包,版本 4.2.4。
在我编写本书时,前述列表中提到的软件包版本都是最新的。
本章中所有代码和数据均可在github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/tree/main/Chapter_4找到。
介绍 ggplot2
通过图表传达信息通常比单独的表格更有效、更具视觉吸引力。毕竟,人类在处理视觉信息方面要快得多,比如在图像中识别一辆汽车。在构建 机器学习(ML)模型时,我们通常对训练和测试损失曲线感兴趣,该曲线以折线图的形式表示随着模型训练时间的延长,训练集和测试集损失逐渐减少。观察性能指标有助于我们更好地诊断模型是否 欠拟合 或 过拟合——换句话说,当前模型是否过于简单或过于复杂。请注意,测试集用于近似未来的数据集,最小化测试集错误有助于模型泛化到新的数据集,这种方法被称为 经验风险最小化。欠拟合是指模型在训练集和测试集上都表现不佳,这是由于拟合能力不足造成的,而过拟合则意味着模型在训练集上表现良好,但在测试集上表现不佳,这是由于模型过于复杂造成的。无论是欠拟合还是过拟合,都会导致测试集上的错误频率高,从而降低泛化能力。
良好的可视化技能也是良好沟通者的标志。创建良好的可视化需要仔细设计界面,同时满足关于可实现性的技术限制。当被要求构建机器学习模型时,大部分时间通常花在数据处理、模型开发和微调上,只留下极小的一部分时间来向利益相关者传达建模结果。有效的沟通意味着即使对于该领域外的人来说,机器学习模型虽然是一个黑盒解决方案,但仍然可以透明且充分地向内部用户解释和理解。由ggplot2等提供的有意义的强大可视化,这是tidyverse生态系统中专注于图形的特定包,是有效沟通的绝佳促进者;其输出通常比基础 R 提供的默认绘图选项更具视觉吸引力和吸引力。毕竟,随着你在企业阶梯上的攀升和更多地从观众的角度思考,创建良好的可视化将成为一项基本技能。良好的演示技巧将和(如果不是比)你的技术技能(如模型开发)同样重要?
本节将向您展示如何通过构建简单而强大的图表来达到良好的视觉沟通效果,使用的是ggplot2包。这将有助于揭开使用 R 的现代可视化技术的神秘面纱,并为您准备更高级的可视化技术。我们将从一个简单的散点图示例开始,并使用包含一系列与汽车相关的观察数据的mtcars数据集介绍ggplot2包的基本绘图语法,该数据集在加载ggplot2时自动加载到工作环境中。
构建散点图
散点图是一种二维图表,其中两个变量的值(通常是数值类型)唯一确定图表上的每个点。当我们想要评估两个数值变量之间的关系时,散点图是首选的图表类型。
让我们通过一个练习来绘制使用mtcars数据集的汽车气缸数(cyl变量)和每加仑英里数(mpg变量)之间的关系图。
练习 4.1 – 使用 mtcars 数据集构建散点图
在这个练习中,我们将首先检查mtcars数据集的结构,并使用ggplot2生成一个双变量散点图。按照以下步骤进行:
-
加载并检查
mtcars数据集的结构,如下所示:>>> library(ggplot2) >>> str(mtcars) 'data.frame': 32 obs. of 11 variables: $ mpg : num 21 21 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 ... $ cyl : num 6 6 4 6 8 6 8 4 4 6 ... $ disp: num 160 160 108 258 360 ... $ hp : num 110 110 93 110 175 105 245 62 95 123 ... $ drat: num 3.9 3.9 3.85 3.08 3.15 2.76 3.21 3.69 3.92 3.92 ... $ wt : num 2.62 2.88 2.32 3.21 3.44 ... $ qsec: num 16.5 17 18.6 19.4 17 ... $ vs : num 0 0 1 1 0 1 0 1 1 1 ... $ am : num 1 1 1 0 0 0 0 0 0 0 ... $ gear: num 4 4 4 3 3 3 3 4 4 4 ... $ carb: num 4 4 1 1 2 1 4 2 2 4 ...结果显示,
mtcarsDataFrame 包含 32 行和 11 列,这是一个相对较小且结构化的数据集,易于处理。接下来,我们将绘制cyl和mpg之间的关系图。 -
使用
ggplot()和geom_point()函数根据cyl和mpg变量生成散点图。使用theme层放大标题和两轴上的文本大小:>>> ggplot(mtcars, aes(x=cyl, y=mpg)) + geom_point() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))如图 4**.1所示,生成的结果包含 32 个点,其位置由
cyl和mpg的组合唯一确定。截图表明,随着cyl的增加,mpg的值呈下降趋势,尽管在cyl的三个组内也存在明显的组内变异:
图 4.1 – cyl 和 mpg 之间的散点图
注意,aes()函数将cyl映射到x轴,将mpg映射到y轴。当映射关系没有明确显示时,我们通常假设第一个参数对应于水平轴,第二个对应于垂直轴。
生成散点图的脚本由两个高级函数组成:ggplot()和geom_point()。ggplot()函数在第一个参数中指定要使用的数据集,在第二个参数中指定分别绘制在两个轴上的变量,使用aes()函数包装(更多内容将在后面介绍)。geom_point()函数强制显示为散点图。这两个函数通过特定的+运算符连接在一起,表示将第二层操作叠加到第一层。
此外,请注意,ggplot()将cyl变量视为数值,如水平轴上的额外标签5和7所示。我们可以通过以下方式验证cyl的独立值:
>>> unique(mtcars$cyl)
6 4 8
显然,我们需要将其视为一个分类变量,以避免不同值之间的不必要插值。这可以通过使用factor()函数包装cyl变量来实现,该函数将输入参数转换为分类输出:
>>> ggplot(mtcars, aes(factor(cyl), mpg)) +
geom_point() +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"))
结果图示在图 4**.2中。通过显式地将cyl转换为分类变量,水平轴正确地表示了每个唯一cyl值的点分布:
图 4.2 – 将 cyl 转换为分类变量后的散点图
到目前为止,我们已经学习了如何通过转换到所需类型后传入感兴趣的变量来构建散点图。这与其他类型的图表类似,遵循一套标准的语法规则。接下来,我们将探讨这些基本规则以了解它们的共性。
理解图形语法
之前的例子包含了在绘图时需要指定的三个基本层:数据、美学和几何形状。每一层的主要目的如下列出:
-
数据层指定要绘制的数据集。这对应于我们之前指定的
mtcars数据集。 -
美学层指定了与缩放相关的项目,这些项目将变量映射到图表的视觉属性。例如,包括用于x轴和y轴的变量、大小和颜色,以及其他图表美学。这对应于我们之前指定的
cyl和mpg变量。 -
几何层指定了用于数据的视觉元素,例如通过点、线或其他形式呈现数据。我们在前面的例子中设置的
geom_point()命令告诉图表以散点图的形式显示。
其他层,如主题层,也有助于美化图表,我们将在后面介绍。
前面的例子中的geom_point()层还暗示我们可以通过更改下划线后的关键字轻松切换到另一种类型的图表。例如,如以下代码片段所示,我们可以使用geom_boxplot()函数将散点图显示为每个独特的cyl值的箱线图:
>>> ggplot(mtcars, aes(factor(cyl), mpg)) +
geom_boxplot() +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"))
执行此命令将生成图 4.3所示的输出,该输出将每个不同的cyl值的一组点作为箱线图进行可视化。使用箱线图是检测异常值(如位于第三个箱线图外的两个极端点)的一种极好方式:
图 4.3 – 使用箱线图可视化相同的图表
类似地,我们可以通过调整美学层来改变之前散点图中点的颜色和大小。让我们通过一个练习来看看如何实现这一点。
练习 4.2 – 改变散点图中点的颜色和大小
在这个练习中,我们将使用美学层根据disp和hp变量修改最后散点图中显示的点的颜色和大小。disp变量衡量发动机排量,而hp变量表示总马力。因此,点的颜色和大小将根据disp和hp的不同值而变化。按照以下步骤进行:
-
通过在
aes()函数中将disp传递给color参数来改变散点图中点的颜色。同时,也将图例的size参数放大:>>> ggplot(mtcars, aes(factor(cyl), mpg, color=disp)) + geom_point() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.4所示的输出,其中每个点的颜色渐变根据
disp的值而变化:
图 4.4 – 向散点图添加颜色
-
通过在
aes()函数中将hp传递给size参数,如下所示,来改变散点图中点的尺寸:>>> ggplot(mtcars, aes(factor(cyl), mpg, color=disp, size=hp)) + geom_point()执行此命令将生成图 4.5所示的输出,其中每个点的尺寸也根据
hp的值而变化:
图 4.5 – 改变散点图中点的尺寸
虽然现在图表看起来更加丰富,但在向单个图表添加维度时要小心。在我们的当前示例中,单个图表包含四个维度的信息:cyl、mpg、disp 和 hp。人类大脑擅长处理二维或三维视觉,但在面对更高维度的图表时可能会感到困难。展示风格取决于我们想要传达给观众的信息。与其将所有维度混合在一起,不如构建一个只包含两个或三个变量的单独图表进行说明可能更有效。记住——在模型开发中,有效的沟通在于传达给观众的信息质量,而不是视觉输出的丰富性。
以下练习将让我们更详细地查看不同层级的各个组件。
练习 4.3 – 使用平滑曲线拟合构建散点图
在这个练习中,我们将构建一个散点图,并拟合一个穿过点的平滑曲线。添加平滑曲线有助于我们检测点之间的整体模式,这是通过使用 geom_smooth() 函数实现的。按照以下步骤进行:
-
使用
hp和mpg构建散点图,并使用geom_smooth()进行平滑曲线拟合,使用disp进行着色,并通过在geom_point()中设置alpha=0.6来调整点的透明度:>>> ggplot(mtcars, aes(hp, mpg, color=disp)) + geom_point(alpha=0.6) + geom_smooth() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行前面的命令会生成图 4.6 所示的输出,其中中心蓝色曲线代表最佳拟合点的模型,周围的界限表示不确定性区间。我们将在后面的章节中更详细地讨论模型的概念:
图 4.6 – 在散点图中拟合点之间的平滑曲线
由于图形是基于叠加层概念构建的,我们也可以通过从一些组件开始,将它们存储在变量中,然后向图形变量添加额外的组件来生成一个图。让我们看看以下步骤是如何实现的。
-
使用与之前相同的透明度级别,使用
hp和mpg构建散点图,并将其存储在plt变量中:>>> plt = ggplot(mtcars, aes(hp, mpg)) + geom_point(alpha=0.6) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold")) >>> plt如图 4.7 所示,直接打印出
plt会生成一个工作图,这表明一个图也可以作为一个对象存储:
图 4.7 – 使用 hp 和 mpg 生成散点图
-
使用
disp着色点,并像这样向之前的图表添加平滑曲线拟合:>>> plt = plt + geom_point(aes(color=disp)) + geom_smooth() >>> plt执行这些命令将生成与图 4.6 所示相同的图。因此,我们可以构建一个基础图,将其保存在变量中,并通过添加额外的图层规格来调整其视觉属性。
我们还可以通过指定相关参数来对散点图中点的尺寸、形状和颜色进行更精细的控制,所有这些都可以在以下练习中完成。
练习 4.4 – 控制散点图中点的尺寸、形状和颜色
在这个练习中,我们将通过不同的输入参数来控制散点图中点的几个视觉属性。这些控制由 geom_point() 函数提供。按照以下步骤进行:
-
生成
hp和mpg之间的散点图,并使用disp为点着色。将点显示为大小为4的圆圈:>>> ggplot(mtcars, aes(hp, mpg, color=disp)) + geom_point(shape=1, size=4) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成 图 4.8 中所示的输出,其中我们看到点被放大成不同颜色的圆圈:
图 4.8 – 使用较大尺寸的圆圈作为点的散点图
注意,在 geom_point() 中设置 shape=1 将点显示为圆圈。我们可以通过更改此参数以其他形式展示它们。例如,以下命令将点可视化成较小尺寸的三角形:
>>> ggplot(mtcars, aes(hp, mpg, color=disp)) +
geom_point(shape=2, size=2) +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
这在 图 4.9 中显示:
图 4.9 – 在散点图中将点可视化成三角形
接下来,我们将探讨如何通过填充点的内部颜色来使散点图更具视觉吸引力。
-
使用
aes()函数中的cyl(在将其转换为因子类型后)填充之前散点图的颜色,并在geom_point()函数中将shape参数设置为21,size设置为5,透明度(通过alpha)设置为0.6:>>> ggplot(mtcars, aes(wt, mpg, fill = factor(cyl))) + geom_point(shape = 21, size = 5, alpha = 0.6) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))观察到 图 4.10 中的输出,现在图表看起来更具视觉吸引力,其中三组点分布在
hp和mpg的不同范围内。一个敏锐的读者可能会想知道为什么我们设置shape=21,而点仍然被可视化成圆圈。这是因为21是一个特殊值,允许填充圆圈的内部颜色,以及它们的轮廓或外部颜色:
图 4.10 – 散点图中点的内部颜色填充
注意,除了在图上可视化点之外,我们还可以将它们作为文本标签来展示,这在特定场景中可能更有信息量。也可能出现多个点重叠的情况,使得难以区分它们。让我们看看如何处理这种情况,并通过以下练习以不同的方式展示点。
练习 4.5 – 散点图中展示点的不同方式
在这个练习中,我们将学习两种不同的方式来展示散点图中的点:显示文本标签和抖动重叠的点。这两种技术都将为我们的绘图工具包增加更多灵活性。按照以下步骤进行:
-
使用
row.names()根据每行的名称可视化品牌名称,并使用geom_text()将它们绘制在hp对mpg的先前散点图上:>>> ggplot(mtcars, aes(hp, mpg)) + geom_text(label=row.names(mtcars)) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4*.11* 所示的输出,其中品牌名称取代了点。然而,一些品牌名称彼此重叠,使得难以识别它们的特定文本。让我们看看如何解决这个问题:
图 4.11 – 在散点图中显示品牌名称
-
通过将
position_jitter()函数传递给geom_text()函数的position参数来调整重叠文本:>>> ggplot(mtcars, aes(hp, mpg)) + geom_text(label=row.names(mtcars), fontface = "bold", position=position_jitter(width=20,height=20)) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4*.12* 所示的输出,其中我们额外指定了
fontface参数为bold以提高清晰度。通过更改position_jitter()函数的width和height参数并将其传递给geom_text()函数的position参数,我们成功调整了图表中文本的位置,使其现在更易于视觉理解:
图 4.12 – 抖动文本的位置
接下来,我们将探讨如何抖动重叠点。
-
按如下方式生成
cyl因素与mpg的散点图:>>> ggplot(mtcars, aes(factor(cyl), mpg)) + geom_point() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4*.13* 所示的输出,其中我们故意使用了
cyl分类型变量来显示多个点在图上重叠:
图 4.13 – 可视化具有重叠点的散点图
让我们调整重叠点的位置,使它们在视觉上可区分,从而给我们一个关于有多少这样的点排列在单个位置上的感觉。请注意,抖动意味着在这种情况下向点添加随机位置调整。
-
使用
geom_jitter()函数对点进行抖动,如下所示:>>> ggplot(mtcars, aes(factor(cyl), mpg)) + geom_jitter() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4*.14* 所示的输出,其中
cyl每个类别的点现在彼此分离,而不是排列在同一条线上。添加随机抖动因此有助于通过随机扰动来视觉上分离重叠的点:
图 4.14 – 随机抖动重叠点
接下来,我们将探讨确定图中显示的视觉元素的图形几何形状。
图形中的几何形状
上一节主要介绍了散点图。在本节中,我们将介绍两种额外的常见图表类型:条形图和折线图。我们将讨论构建这些图表的不同方法,重点关注可以用来控制图形特定视觉属性的几何形状。
理解散点图中的几何关系
让我们回顾一下散点图,并放大几何层。几何层决定了图表的实际外观,这是我们视觉交流中的基本层。在撰写本文时,我们有超过 50 种几何形状可供选择,所有这些都以 geom_ 关键字开头。
在决定使用哪种几何形状时,有一些总体指南适用。例如,以下列表包含典型散点图可能适用的几何形状类型:
-
点,将数据可视化表示为点
-
抖动,向散点图添加位置抖动
-
拟合线,在散点图上添加一条线
-
平滑,通过拟合趋势线并添加置信界限来平滑图表,以帮助识别数据中的特定模式
-
计数,在散点图的每个位置计数并显示观测值的数量
每个几何层都与其自己的美学配置相关联,包括强制性和可选设置。例如,geom_point() 函数需要 x 和 y 作为强制参数来唯一定位图表上的点,并允许可选设置,如 alpha 参数来控制透明度级别,以及 color 和 fill 来管理点的着色,以及它们的 shape 和 size 参数,等等。
由于几何层提供层特定控制,我们可以在美学层或几何层中设置一些视觉属性。例如,以下代码生成了与图4.15中显示的相同图表,其中着色可以在基本的 ggplot() 函数或特定于层的 geom_point() 函数中设置:
>>> ggplot(mtcars, aes(hp, mpg, color=factor(cyl))) +
geom_point() +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
>>> ggplot(mtcars, aes(hp, mpg)) +
geom_point(aes(col=factor(cyl))) +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
这会产生以下图表:
图 4.15 – 使用特定于层的几何控制生成相同的散点图
当我们在图表中显示多个层(不一定是不同类型)时,层特定控制带来的灵活性就显现出来了。在接下来的练习中,我们将看到如何一起使用多个几何层。
练习 4.6 – 使用多个几何层
在这个练习中,我们将在之前的散点图上显示不同 cyl 组的 hp 和 mpg 的平均值。一旦从原始 mtcars 数据集中获得,可以通过叠加另一个几何层,采用相同类型的散点图来添加额外的平均统计信息。按照以下步骤进行:
-
使用
dplyr库计算每个cyl组所有列的平均值,并将结果存储在一个名为tmp的变量中:>>> library(dplyr) >>> tmp = mtcars %>% group_by(factor(cyl)) %>% summarise_all(mean) >>> tmp # A tibble: 3 × 12 `factor(cyl)` mpg cyl disp hp drat wt qsec <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> 1 4 26.7 4 105. 82.6 4.07 2.29 19.1 2 6 19.7 6 183\. 122. 3.59 3.12 18.0 3 8 15.1 8 353\. 209. 3.23 4.00 16.8 # … with 4 more variables: vs <dbl>, am <dbl>, # gear <dbl>, carb <dbl>我们可以看到,使用
summarize_all()函数获取所有列的平均值的摘要统计信息,这是一个将输入函数应用于每个组的所有列的实用函数。在这里,我们传递mean函数来计算列的平均值。结果存储在tmp中的tibble对象包含了cyl三个组中所有变量的平均值。需要注意的是,在添加额外的几何层时,基础美学层期望每个几何层中具有相同的列名。在
ggplot()函数中的基础美学层适用于所有几何层。让我们看看如何添加一个额外的几何层作为散点图来展示不同cyl组中平均hp和mpg值。 -
添加一个额外的散点图层来展示每个
cyl组的平均hp和mpg值作为大正方形:>>> ggplot(mtcars, aes(x=hp, y=mpg, color=factor(cyl))) + geom_point() + geom_point(data=tmp, shape=15, size=6) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.16中所示的输出,其中大正方形(通过在第二个
geom_point层中设置shape=15和size=6获得)来源于tmp数据集,这是通过附加几何层中的data参数指定的。注意,平均
hp和mpg值会自动左连接到现有数据集中,这显示了每个cyl组中不同的hp和mpg值。为了确保两个几何层在绘图时相互兼容,我们需要确保所有匹配的坐标(x和y参数)存在于相应的原始数据集中:
图 4.16 – 可视化每个 cyl 组的平均 hp 和 mpg 值
此图由两个几何层组成,其中第一层将每个观测值绘制为小圆圈,第二层将每个cyl组的hp和mpg的平均值绘制为大正方形。添加额外层遵循相同的原理,只要每个层的源数据包含基础美学层中指定的列名。
为了进一步说明多个层需要匹配坐标的需求,让我们在控制台中尝试输入以下命令,其中我们只选择传递给第二个几何层的原始数据中的mpg和disp列。如输出所示,期望有hp列,如果没有它将抛出错误:
>>> ggplot(mtcars, aes(x=hp, y=mpg, color=factor(cyl))) +
geom_point() +
geom_point(data=tmp[,c("mpg","disp")], shape=15, size=6)
Error in FUN(X[[i]], ...) : object 'hp' not found
在下一节中,我们将探讨一种新的绘图类型:条形图,以及与其相关的几何层。
引入条形图
条形图以条形的形式显示分类或连续变量的某些统计信息(如频率或比例)。在多种类型的条形图中,直方图是一种特殊的条形图,它显示了单个连续变量的分箱分布。因此,绘制直方图始终涉及一个连续输入变量,使用 geom_histogram() 函数并仅指定 x 参数来实现。在内部,该函数首先将连续输入变量切割成离散的箱。然后,它使用内部计算的 count 变量来指示每个箱中要传递给 y 参数的观测数。
让我们看看如何在以下练习中构建直方图。
练习 4.7 – 构建直方图
在这个练习中,我们将探讨在显示直方图时应用位置调整的不同方法。按照以下步骤进行:
-
使用
geom_histogram()层构建hp变量的直方图,如下所示:>>> ggplot(mtcars, aes(x=hp)) + geom_histogram() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold")) `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.执行此命令将生成 图 4.17 中所示的输出,以及关于分箱的警告信息。这是因为默认的分箱值不适合,因为条形之间存在多个间隙,这使得对连续变量的解释变得困难。我们需要使用
binwidth参数微调每个箱的宽度:
图 4.17 – 为 hp 绘制直方图
-
调整
binwidth参数以使直方图连续并移除警告信息,如下所示:>>> ggplot(mtcars, aes(x=hp)) + geom_histogram(binwidth=40) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))这将产生以下输出:
图 4.18 – 显示连续直方图
制作看起来连续的直方图取决于数据,并且需要尝试和错误。在这种情况下,设置 binwidth=40 似乎对我们有效。
接下来,我们将通过更改条形的着色将分组引入之前的直方图。
-
根据因子的
cyl使用fill参数以不同颜色填充条形:>>> ggplot(mtcars, aes(x=hp, fill=factor(cyl))) + geom_histogram(binwidth=40) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成 图 4.19 中所示的输出,其中每个条形代表不同的
cyl组。然而,一个机敏的读者可能会立即发现,对于某些有两种颜色的条形,很难判断它们是重叠的还是堆叠在一起的。确实,直方图的默认设置是position="stack",这意味着条形默认是堆叠的。为了消除这种混淆,我们可以明确地显示条形并排:
图 4.19 – 为直方图的条形着色
-
通过设置
position="dodge"来并排显示条形,如下所示:>>> ggplot(mtcars, aes(x=hp, fill=factor(cyl))) + geom_histogram(binwidth=40, position="dodge") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成 图 4.20 中所示的输出,其中条形现在并排显示。我们可以进一步调整
binwidth参数以减少条形之间的间隙:
图 4.20 – 并排条形图
最后,我们还可以将统计数据以比例而不是计数的形式显示。
-
通过执行以下代码来显示按
cyl分类的hp的前一个直方图作为比例:>>> ggplot(mtcars, aes(x=hp, fill=factor(cyl))) + geom_histogram(binwidth=40, position="fill") + ylab("Proportion") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.21中所示的输出,其中
ylab()函数用于更改y轴的标签。由于每个箱子的比例需要加起来等于1,因此图表包含等高的条形,每个条形包含一个或多个组。对于包含多个组的每个箱子,每种颜色的高度代表落在该特定cyl组内的观测值的比例。这种图表通常在我们只关心每个组的相对百分比而不是绝对计数时使用:
图 4.21 – 以比例显示条形图
如前所述,直方图是一种特殊的条形图。经典的条形图包含x轴上的分类变量,其中每个位置代表落在该特定类别中的观测数的计数。可以使用geom_bar()函数生成条形图,该函数允许与geom_histogram()相同的定位调整。让我们通过以下练习来学习其用法。
练习 4.8 – 构建条形图
在这个练习中,我们将通过cyl和gear来可视化观测值的计数作为条形图。按照以下步骤进行:
-
使用
cyl作为x轴,在堆叠条形图中绘制每个独特的cyl和gear组合的观测值计数。>>> ggplot(mtcars, aes(x=factor(cyl), fill=factor(gear))) + geom_bar(position="stack") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.22中所示的输出,其中条形的高度代表特定
cyl和gear组合的观测数计数:
图 4.22 – 按 cyl 和 gear 堆叠的条形图
我们还可以使用各自组中观测值的比例/百分比来表示条形图。
-
将条形图转换为基于百分比的图表,以显示每个组合的分布,如下所示:
>>> ggplot(mtcars, aes(x=factor(cyl), fill=factor(gear))) + geom_bar(position="fill") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.23中所示的输出:
图 4.23 – 将条形图可视化成比例
如前所述,我们还可以将条形图从堆叠转换为并排。
-
按照以下方式将之前的信息可视化成并排条形图:
>>> ggplot(mtcars, aes(x=factor(cyl), fill=factor(gear))) + geom_bar(position="dodge") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成图 4.24中所示的输出:
图 4.24 – 并排条形图
我们还可以自定义条形图,使条形部分重叠。这可以通过使用position_dodge()函数实现,如下所示,其中我们调整width参数以将重叠的条形抖动到一定程度:
>>> ggplot(mtcars, aes(x=factor(cyl), fill=factor(gear))) +
geom_bar(position = position_dodge(width=0.2)) +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
执行此命令将生成图 4.25中显示的输出:
图 4.25 – 调整柱状图中重叠的条形
接下来,我们将查看另一种流行的绘图类型:线形图。
介绍线形图
线形图显示一个变量的值随着另一个变量的变化而变化。与散点图一样,线形图可以被认为是通过线连接的散点。它主要用于描述两个变量之间的关系。例如,当两个变量相互正相关时,增加一个变量会导致另一个变量似乎成比例增加。在线形图上可视化这种关系可能会在两个变量之间产生一个正斜率的趋势线。
线形图中最广泛使用的一种类型是时间序列图,其中特定指标(如股价)的值被显示为时间的函数(如每日)。在下面的练习中,我们将使用由 base R 提供的JohnsonJohnson数据集,查看 1960 年至 1981 年间 Johnson & Johnson 的季度收益。我们将探索不同的可视化线形图的方法,以及一些针对时间序列数据的数据处理。
练习 4.9 – 绘制时间序列图
在这个练习中,我们将查看将时间序列数据可视化成线形图。按照以下步骤进行:
-
通过执行以下代码来检查
JohnsonJohnson数据集的结构:>>> str(JohnsonJohnson) Time-Series [1:84] from 1960 to 1981: 0.71 0.63 0.85 0.44 0.61 0.69 0.92 0.55 0.72 0.77 ...输出表明该数据集是一个从
1960到1981的单变量(即单一变量)时间序列。打印其内容(仅显示前五行)也告诉我们,频率是季度性的,使用年-季度作为时间序列中每个数据点的唯一索引:>>> JohnsonJohnson Qtr1 Qtr2 Qtr3 Qtr4 1960 0.71 0.63 0.85 0.44 1961 0.61 0.69 0.92 0.55 1962 0.72 0.77 0.92 0.60 1963 0.83 0.80 1.00 0.77 1964 0.92 1.00 1.24 1.00让我们将它转换为熟悉的 DataFrame 格式,以便于数据操作。
-
将其转换为名为
JohnsonJohnson2的 DataFrame,包含两列:qtr_earning用于存储季度时间序列,date用于存储近似日期:>>> library(zoo) >>> JohnsonJohnson2 = data.frame(qtr_earning=as.matrix(JohnsonJohnson), date=as.Date(as.yearmon(time(JohnsonJohnson)))) >>> head(JohnsonJohnson2, n=3) qtr_earning date 1 0.71 1960-01-01 2 0.63 1960-04-01 3 0.85 1960-07-01date列是通过从JohnsonJohnson时间序列对象中提取time索引得到的,使用as.yearmon()显示为年月格式,然后使用as.Date()转换为日期格式。我们还将添加两个额外的指示列,用于后续的绘图。
-
添加一个
ind指示列,如果日期等于或晚于1975-01-01,则其值为TRUE,否则为FALSE。同时,从date变量中提取季度并存储在qtr变量中:>>> JohnsonJohnson2 = JohnsonJohnson2 %>% mutate(ind = if_else(date >= as.Date("1975-01-01"), TRUE, FALSE), qtr = quarters(date)) >>> head(JohnsonJohnson2, n=3) qtr_earning date ind qtr 1 0.71 1960-01-01 FALSE Q1 2 0.63 1960-04-01 FALSE Q2 3 0.85 1960-07-01 FALSE Q3在此命令中,我们使用了
quarters()函数从一个日期格式化的字段中提取季度。接下来,我们将绘制季度收益作为时间序列。 -
使用线形图将
qtr_earning作为date的函数进行绘图,如下所示:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning)) + geom_line() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令将生成如图 图 4*.26* 所示的输出,其中我们将
date列指定为 x 轴,将qtr_earning指定为 y 轴,然后是geom_line()层:
图 4.26 – 季度收益的时间序列图
季度收益的线图显示长期上升趋势和短期波动。时间序列预测的主题集中在使用这些结构组件(如趋势和季节性)来预测未来值。
此外,我们还可以对时间序列进行着色编码,以便不同的线段根据另一个分组变量显示不同的颜色。
-
根据列
ind指定线图的颜色,如下所示:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=ind)) + geom_line() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成如图 图 4*.27* 所示的输出,其中我们在基础美学层中设置
color=ind以更改颜色。请注意,由于这两个线段实际上是图表上分别绘制的独立时间序列,因此它们是断开的:
图 4.27 – 两种不同颜色的线图
当分组变量中有多个类别时,我们也可以绘制多条线,每条线将假设不同的颜色。
-
分别绘制每个季度的时序图,如下所示:
>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行前面的命令将生成如图 图 4*.28* 所示的输出,其中
1980:
图 4.28 – 每个季度的年度时序图
在下一节中,我们将查看主题层,它控制图形的样式元素。
控制图形中的主题
主题层指定了图上所有非数据相关的属性,如背景、图例、轴标签等。适当控制图中的主题可以通过突出关键信息并引导用户注意我们想要传达的信息来帮助视觉沟通。
主题层控制了以下三种类型的视觉元素,如下所示:
-
文本,用于指定轴标签的文本显示(例如,颜色)
-
行,用于指定轴的视觉属性,如颜色和线型
-
矩形,用于控制图形的边框和背景
所有三种类型都使用以 element_ 开头的函数指定,包括例如 element_text() 和 element_line() 的示例。我们将在下一节中介绍这些函数。
调整主题
主题层可以轻松地作为现有图的一个附加层应用。让我们通过一个练习来看看如何实现这一点。
练习 4.10 – 应用主题
在这个练习中,我们将查看如何调整之前时间序列图的与主题相关的元素,包括移动图例和更改轴的属性。按照以下步骤进行:
-
通过叠加一个主题层,并将其
legend.position参数指定为"bottom",在底部显示前一个时间序列图的图例。同时,增大轴和图例中文字的字体大小:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme(legend.position="bottom", axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令将生成如图图 4.29所示的输出,其中图例现在被移动到图的底部:
图 4.29 – 在底部显示图例
我们也可以通过向legend.position参数提供坐标信息来将图例放置在图的任何位置。坐标从左下角开始,值为(0,0),一直延伸到右上角,值为(1,1)。由于图的左上部分看起来比较空旷,我们可能考虑将图例移动到那里以节省一些额外空间。
-
通过提供一对适当的坐标来将图例移动到左上角:
>>> tmp = ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme(legend.position=c(0.1,0.8), axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20)) >>> tmp在这里,我们将图例的位置指定为
(0.1, 0.8)。通常,使用坐标系配置适当的位置需要尝试和错误。我们还将结果保存在名为tmp的变量中,稍后将使用它。生成的图如图图 4.30所示:
图 4.30 – 基于坐标的位置调整图例
接下来,我们将调整轴的属性。
-
基于前一个图,使用
element_text()函数在axis.title属性上更改轴标题的颜色为蓝色。同时,使用element_line()函数在axis.line属性上使轴的线条为实线黑色:>>> tmp = tmp + theme( axis.title=element_text(color="blue"), axis.line = element_line(color = "black", linetype = "solid") ) >>> tmp执行此命令将生成如图图 4.31所示的输出,其中我们使用了
element_text()和element_line()函数来调整标题(axis.title)和轴的线条(axis.line)的视觉属性(color和linetype):
图 4.31 – 更改轴的标题和线条
最后,我们还可以更改默认的背景和网格。
-
通过执行以下代码来移除前一个图中默认的网格和背景:
>>> tmp = tmp + theme( panel.grid.major = element_blank(), panel.grid.minor = element_blank(), panel.background = element_blank() ) >>> tmp在这里,我们使用
panel.grid.major和panel.grid.minor来访问网格属性,使用panel.background来访问图的背景属性。element_blank()移除所有现有配置,并指定为这三个属性。结果如图图 4.32所示:
图 4.32 – 移除网格和背景设置
注意,我们还可以将主题层保存到变量中,并将其作为叠加应用到其他图中。我们将整个图或特定的图层配置作为一个变量,这使得将其扩展到多个图变得方便。
除了创建我们自己的主题之外,我们还可以利用 ggplot2 提供的内置主题层。如列表所示,这些内置主题提供了现成的解决方案,以简化绘图:
-
theme_gray(),我们之前使用的默认主题 -
theme_classic(),在科学绘图中最常用的传统主题 -
theme_void(),它移除了所有非数据相关的属性 -
theme_bw(),主要用于配置透明度级别时
例如,我们可以使用 theme_classic() 函数生成与之前相似的图表,如下面的代码片段所示:
>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning,
color=qtr)) +
geom_line() +
theme_classic() +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.text = element_text(size=20))
执行此命令会生成如图 图 4*.33* 所示的输出:
图 4.33 – 使用现成的主题设置
除了内置的主题之外,ggthemes 包还提供了额外的主题,进一步扩展了我们的主题选择。让我们在下一节中探索这个包。
探索 ggthemes
ggthemes 包包含多个预构建的主题。就像使用 dplyr 可以显著加速我们的数据处理任务一样,使用预构建的主题也可以与从头开始开发相比,简化我们的绘图工作。让我们看看这个包中可用的几个主题。
练习 4.11 – 探索主题
在这个练习中,我们将探索 ggthemes 提供的一些额外的现成主题。记住在继续下面的代码示例之前,下载并加载这个包。我们将涵盖两个主题函数。按照以下步骤进行:
-
在上一个图表上应用
theme_fivethirtyeight主题,如下所示:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme_fivethirtyeight() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令会生成如图 图 4*.34* 所示的输出,其中图例位于底部:
图 4.34 – 应用 theme_fivethirtyeight 主题
-
应用
theme_tufte()主题,如下所示:>>> ggplot(JohnsonJohnson2, aes(x=date, y=qtr_earning, color=qtr)) + geom_line() + theme_tufte() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令会生成如图 图 4*.35* 所示的输出,这是科学论文中常用的绘图类型。请注意,学术论文中的图表建议只显示必要的信息。这意味着背景等额外配置是不被鼓励的。另一方面,现实生活中的图表则更倾向于在实用性和美观性之间保持一个合理的平衡:
图 4.35 – 应用 theme_tufte 主题
在本节中,我们探讨了控制图表中与主题相关的元素,这在我们进行微调和自定义图表时提供了很大的灵活性。
概述
在本章中,我们介绍了基于ggplot2包的基本图形技术。我们首先回顾了基本的散点图,并学习了在图表中开发层面的语法。为了构建、编辑和改进一个图表,我们需要指定三个基本层面:数据、美学和几何。例如,用于构建散点图的geom_point()函数允许我们控制图表上点的尺寸、形状和颜色。我们还可以使用geom_text()函数将它们显示为文本,除了使用点来表示之外。
我们还介绍了由几何层提供的层特定控制,并展示了使用条形图和折线图的示例。条形图可以帮助表示分类变量的频率分布和连续变量的直方图。折线图支持时间序列数据,并且如果绘制得当,可以帮助识别趋势和模式。
最后,我们还介绍了主题层,它允许我们控制图表中所有与数据无关的视觉方面。结合基础 R 的内置主题和ggthemes的现成主题,我们有多种选择,可以加速绘图工作。
在下一章中,我们将介绍探索性数据分析(EDA),这是许多数据分析建模任务中常见且必要的一步。
第五章:探索性数据分析
上一章介绍了使用ggplot2的基本绘图原则,包括各种几何形状和主题层的应用。结果证明,清理和整理原始数据(在第第二章和第三章中介绍)以及数据可视化(在第第四章中介绍)属于典型数据科学项目工作流程的第一阶段——即探索性数据分析(EDA)。我们将通过本章的一些案例研究来介绍这一内容。我们将学习如何应用本书前面介绍过的编码技术,并专注于通过 EDA 的视角分析数据。
在本章结束时,你将了解如何使用数值和图形技术揭示数据结构,发现变量之间的有趣关系,以及识别异常观测值。
在本章中,我们将涵盖以下主题:
-
EDA 基础
-
实践中的 EDA(探索性数据分析)
技术要求
要完成本章的练习,你需要具备以下条件:
-
在撰写本文时,
yfR包的最新版本为 1.0.0 -
在撰写本文时,
corrplot包的最新版本为 0.92
本章的代码和数据可在以下链接找到:github.com/PacktPublishing/The-Statistics-and-Machine-Learning-with-R-Workshop/blob/main/Chapter_5/chapter_5.R。
EDA 基础
当面对以表格(DataFrame)形式存在于 Excel 中的新数据集或数据集时,EDA 帮助我们洞察数据集中变量的潜在模式和异常。这是在构建任何预测模型之前的一个重要步骤。俗话说,垃圾输入,垃圾输出。当用于模型开发输入变量存在问题时,如缺失值或不同尺度,所得到的模型可能会表现不佳、收敛缓慢,甚至在训练阶段出现错误。因此,理解你的数据并确保原材料是正确的,是保证模型后期表现良好的关键步骤。
这就是 EDA 发挥作用的地方。EAD(探索性数据分析)不是一种僵化的统计程序,而是一套探索性分析,它使你能够更好地理解数据中的特征和潜在关系。它作为过渡性分析,指导后续建模,涉及我们之前学到的数据操作和可视化技术。它通过各种形式的视觉辅助工具帮助总结数据的显著特征,促进重要特征的提取。
EDA 有两种主要类型:如均值、中位数、众数和四分位数范围等描述性统计,以及如密度图、直方图、箱线图等图形描述。
一个典型的 EDA 流程包括分析分类和数值变量,包括在单变量分析中独立分析以及在双变量和多变量分析中结合分析。常见做法包括分析一组给定变量的分布,并检查缺失值和异常值。在接下来的几节中,我们将首先分析不同类型的数据,包括分类和数值变量。然后,我们将通过案例研究来应用和巩固前几章中涵盖的技术,使用 dplyr 和 ggplot2。
分析分类数据
在本节中,我们将探讨如何通过图形和数值总结来分析两个分类变量。我们将使用来自漫威漫画宇宙的漫画角色数据集,如果你是漫威超级英雄的粉丝,这个数据集可能对你来说并不陌生。该数据集由 read_csv() 函数发布,该函数来自 readr 包,它是 tidyverse 宇宙的数据加载部分,如下代码片段所示:
>>> library(readr)
>>> df = read_csv("https://raw.githubusercontent.com/fivethirtyeight/data/master/comic-characters/marvel-wikia-data.csv")
>>> head(df,5)
# A tibble: 16,376 × 13
page_id name urlslug ID ALIGN EYE HAIR SEX GSM ALIVE APPEARANCES
<dbl> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <dbl>
1 1678 "Spider-Man (Pet… "\\/Sp… Secr… Good… Haze… Brow… Male… NA Livi… 4043
2 7139 "Captain America… "\\/Ca… Publ… Good… Blue… Whit… Male… NA Livi… 3360
3 64786 "Wolverine (Jame… "\\/Wo… Publ… Neut… Blue… Blac… Male… NA Livi… 3061
4 1868 "Iron Man (Antho… "\\/Ir… Publ… Good… Blue… Blac… Male… NA Livi… 2961
5 2460 "Thor (Thor Odin… "\\/Th… No D… Good… Blue… Blon… Male… NA Livi… 2258
6 2458 "Benjamin Grimm … "\\/Be… Publ… Good… Blue… No H… Male… NA Livi… 2255
7 2166 "Reed Richards (… "\\/Re… Publ… Good… Brow… Brow… Male… NA Livi… 2072
8 1833 "Hulk (Robert Br… "\\/Hu… Publ… Good… Brow… Brow… Male… NA Livi… 2017
9 29481 "Scott Summers (… "\\/Sc… Publ… Neut… Brow… Brow… Male… NA Livi… 1955
10 1837 "Jonathan Storm … "\\/Jo… Publ… Good… Blue… Blon… Male… NA Livi… 1934
# … with 16,366 more rows, and 2 more variables: `FIRST APPEARANCE` <chr>, Year <dbl>
打印 DataFrame 显示,该数据集包含 16,376 行和 13 列,包括角色名称、ID 等等。
在下一节中,我们将探讨如何使用计数统计量来总结两个分类变量。
使用计数总结分类变量
在本节中,我们将介绍分析两个分类变量的不同方法,包括使用列联表和条形图。列联表是展示两个分类变量每个唯一组合中观测值总计数的有用方式。让我们通过一个练习来了解如何实现这一点。
练习 5.1 – 总结两个分类变量
在这个练习中,我们将关注两个分类变量:ALIGN(表示角色是好人、中立还是坏人)和 SEX(表示角色的性别)。首先,我们将查看每个变量的唯一值,然后总结组合后的相应总计数:
-
检查
ALIGN和SEX的唯一值:>>> unique(df$ALIGN) "Good Characters" "Neutral Characters" "Bad Characters" NA >>> unique(df$SEX) "Male Characters" "Female Characters" "Genderfluid Characters" "Agender Characters" NA结果显示,这两个变量都包含
NA值。让我们删除ALIGN或SEX中任一包含NA值的观测值。 -
使用
filter语句函数在df中删除ALIGN或SEX中包含NA值的观测值:>>> df = df %>% filter(!is.na(ALIGN), !is.na(SEX))我们可以通过检查结果 DataFrame 的维度和
ALIGN和SEX中NA值的计数来验证是否已成功删除包含NA值的行:>>> dim(df) 12942 13 >>> sum(is.na(df$ALIGN)) 0 >>> sum(is.na(df$SEX)) 0接下来,我们必须创建一个列联表来总结每个唯一值组合的频率。
-
创建
ALIGN和SEX之间的列联表:>>> table(df$ALIGN, df$SEX) Agender Characters Female Characters Genderfluid Characters Male Characters Bad Characters 20 976 0 5338 Good Characters 10 1537 1 2966 Neutral Characters 13 640 1 1440我们可以看到,大多数角色是男性且是坏的。在所有男性角色中,大多数是坏的,而女性角色中好的或中性的角色占主导地位。让我们使用条形图直观地呈现和分析比例。
-
使用
ggplot2在这两个变量之间创建一个条形图:>>> library(ggplot2) >>> ggplot(df, aes(x=SEX, fill=ALIGN)) + geom_bar() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.position = c(0.2, 0.8), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20)) Figure 5*.1*. Here, we used the properties in the theme layer to adjust the size of labels on the graph. For example, axis.text and axis.title are used to increase the size of texts and titles along the axes, legend.position is used to move the legend to the upper-left corner, and legend.key.size and legend.text are used to enlarge the overall display of the legend:
图 5.1 – ALIGN 和 SEX 的条形图
由于Agender Characters和Genderfluid Characters的总计数非常有限,我们可以在绘制条形图时移除这两个组合:
>>> df %>%
filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>%
ggplot(aes(x=SEX, fill=ALIGN)) +
geom_bar()
运行此命令生成图 5.2:
图 5.2 – 从条形图中移除低计数组合
使用计数在比较不同的组合时可能并不直观。在这种情况下,将计数转换为比例将有助于在相对尺度上呈现信息。
将计数转换为比例
在本节中,我们将回顾一个涵盖列联表中条件比例的练习。与之前的无条件列联表不同,在双向列联表的任一维度上进行条件处理会导致比例分布的不同。
练习 5.2 – 总结两个分类变量
在这个练习中,我们将学习如何使用比例表达之前的列联表,并将其转换为基于指定维度的条件分布:
-
使用比例表达之前的列联表。避免使用科学记数法(例如,e+10)并保留三位小数:
>>> options(scipen=999, digits=3) >>> count_df = table(df$ALIGN, df$SEX) >>> prop.table(count_df) Agender Characters Female Characters Genderfluid Characters Male Characters Bad Characters 0.0015454 0.0754134 0.0000000 0.4124556 Good Characters 0.0007727 0.1187606 0.0000773 0.2291763 Neutral Characters 0.0010045 0.0494514 0.0000773 0.1112656列联表中的值现在表示为比例。由于比例是通过将之前的绝对计数除以总和得到的,我们可以通过求和表中的所有值来验证比例的总和是否等于一:
>>> sum(prop.table(count_df)) 1 -
在对行(此处为
ALIGN变量)进行条件处理后,获取作为比例的列联表:>>> prop.table(count_df, margin=1) Agender Characters Female Characters Genderfluid Characters Male Characters Bad Characters 0.003158 0.154089 0.000000 0.842753 Good Characters 0.002215 0.340496 0.000222 0.657067 Neutral Characters 0.006208 0.305635 0.000478 0.687679我们可以通过计算行的求和来验证条件:
>>> rowSums(prop.table(count_df, margin=1)) Bad Characters Good Characters Neutral Characters 1 1 1在此代码中,设置
margin=1表示行级条件。我们也可以通过设置margin=2进行列级条件练习。 -
在对列(例如,
SEX变量)进行条件处理后,获取作为比例的列联表:>>> prop.table(count_df, margin=2) Agender Characters Female Characters Genderfluid Characters Male Characters Bad Characters 0.465 0.310 0.000 0.548 Good Characters 0.233 0.487 0.500 0.304 Neutral Characters 0.302 0.203 0.500 0.148同样,我们可以通过计算列的求和来验证条件:
>>> colSums(prop.table(count_df, margin=2)) Agender Characters Female Characters Genderfluid Characters Male Characters 1 1 1 1 -
在对
SEX应用相同的过滤条件后,在条形图中绘制无条件的比例。将Y轴的标签改为比例:>>> df %>% filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>% ggplot(aes(x=SEX, fill=ALIGN)) + geom_bar(position="fill") + ylab("proportion") + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20))运行此命令生成图 5.3,其中很明显,坏角色主要是男性角色:
图 5.3 – 在条形图中可视化无条件的比例
我们也可以通过在条形图中切换这两个变量从不同的角度获得类似的结果:
>>> df %>%
filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>%
ggplot(aes(x=ALIGN, fill=SEX)) +
geom_bar(position="fill") +
ylab("proportion") +
theme(axis.text=element_text(size=18),
axis.title=element_text(size=18,face="bold"),
legend.key.size = unit(2, 'cm'),
legend.text = element_text(size=20))
运行此命令会生成 图 5*.4*,其中 ALIGN 被用作 x 轴,而 SEX 被用作分组变量:
图 5.4 – 条形图中切换变量
接下来,我们将探讨使用边缘分布和分面条形图描述一个分类变量。
边缘分布和分面条形图
边缘分布指的是在整合其他变量后的一个变量的分布。这意味着我们感兴趣的是某个特定变量的分布,无论其他变量如何分布。
在我们之前存储在 count_df 中的双向列联表中,我们可以通过对所有可能的 ALIGN 值求和来推导出 SEX 的边缘分布,以频率计数的形式。也就是说,我们可以执行列求和以获得 SEX 的边缘计数,如下面的代码片段所示:
>>> colSums(count_df)
Agender Characters Female Characters Genderfluid Characters Male Characters
43 3153 2 9744
这与直接获取 SEX 中不同类别的计数具有相同的效果:
>>> table(df$SEX)
Agender Characters Female Characters Genderfluid Characters Male Characters
43 3153 2 9744
现在,如果我们想为另一个变量的每个类别获取一个变量的边缘分布怎么办?这可以通过 ggplot2 实现,如下面的代码片段所示:
>>> df %>%
filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>%
ggplot(aes(x=SEX)) +
geom_bar() +
facet_wrap(~ALIGN) +
theme(axis.text=element_text(size=15),
axis.title=element_text(size=15,face="bold"),
strip.text.x = element_text(size = 30))
运行此代码生成 图 5*.5*,其中包含三个并排的条形图,分别表示坏、好和中性角色。这实际上是重新排列了 图 5*.4* 中的堆叠条形图。请注意,可以通过使用 facet_wrap 函数添加分面,其中 ~ALIGN 表示分面将使用 ALIGN 变量执行。请注意,我们使用了 strip.text.x 属性来调整分面网格标签的文本大小:
图 5.5 – 分面条形图
此外,我们还可以通过在将其转换为因子后覆盖 ALIGN 的级别来调整单个条形分面的顺序:
>>> df$ALIGN = factor(df$ALIGN, levels = c("Bad Characters", "Neutral Characters", "Good Characters"))
再次运行相同的分面代码现在将生成 图 5*.6*,其中分面的顺序是根据 ALIGN 中的级别确定的:
图 5.6 – 在分面条形图中排列分面的顺序
在下一节中,我们将探讨探索数值变量的不同方法。
分析数值数据
在本节中,我们将探讨使用不同类型的图表来总结 Marvel 数据集中的数值数据。由于数值/连续变量可以假设的值有无限多,因此之前使用的频率表不再适用。相反,我们通常将值分组到预先指定的区间中,这样我们就可以处理范围而不是单个值。
练习 5.3 – 探索数值变量
在这个练习中,我们将使用点图、直方图、密度图和箱线图来描述 Year 变量的数值变量:
-
使用
summary()函数获取Year变量的摘要:>>> summary(df$Year) Min. 1st Qu. Median Mean 3rd Qu. Max. NA's 1939 1973 1989 1984 2001 2013 641 -
生成
Year变量的点状图:>>> ggplot(df, aes(x=Year)) + geom_dotplot(dotsize=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令生成图 5.7,其中每个点代表在x轴对应位置上的一个观测值。相似观测值随后堆叠在顶部。需要注意的是,当观测值数量变得很大时,使用点图可能不是最佳选择,因为由于
ggplot2的技术限制,y轴变得没有意义:
图 5.7 – 使用点图总结年份变量
-
构建
Year变量的直方图:>>> ggplot(df, aes(x=Year)) + geom_histogram() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令生成图 5.8,其中
Year的每个值被分组到不同的区间,然后计算每个区间内的观测值数量以表示每个区间的宽度。请注意,默认的区间数量是 30,但可以使用bins参数覆盖。因此,直方图展示了底层变量的分布形状。我们还可以将其转换为密度图以平滑区间之间的步骤:
图 5.8 – 使用直方图总结年份变量
-
构建
Year变量的密度图:>>> ggplot(df, aes(x=Year)) + geom_density() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))执行此命令生成图 5.9,其中分布以平滑的线条表示。请注意,当数据集中有大量观测值时,建议使用密度图:
图 5.9 – 使用密度图总结年份变量
-
构建
Year变量的箱线图:>>> ggplot(df, aes(x=Year)) + geom_boxplot() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"))
图 5.10 – 使用箱线图总结年份变量
执行此命令生成图 5.10,其中中间的箱体代表大多数观测值(第 25 到第 75 百分位数),箱体中的中线表示中位数(第 50 百分位数),而延伸的胡须包括几乎所有“正常”的观测值。在这个例子中,异常观测值(没有异常值)将表示为胡须范围之外的点。
我们也可以通过SEX添加一个分面层,并观察不同性别下箱线图的变化。
-
在之前的箱线图中使用
SEX变量添加一个分面层:>>> ggplot(df, aes(x=Year)) + geom_boxplot() + facet_wrap(~SEX) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), strip.text.x = element_text(size = 30))执行此命令生成图 5.11。如图所示,大多数女性角色比许多男性角色晚出现,并且近年来女性角色比男性角色多:
图 5.11 – 根据性别分面箱线图
在下一节中,我们将探讨如何可视化高维数据。
高维可视化
之前的例子使用了分面来展示数值变量在每个分类变量的唯一值中的分布。当存在多个分类变量时,我们可以应用相同的技巧并相应地扩展分面。这使我们能够在包含多个分类变量的更高维度中可视化相同的数值变量。让我们通过一个可视化Year按ALIGN和SEX分布的练习来了解。
练习 5.4 – 可视化Year按ALIGN和SEX
在这个练习中,我们将使用ggplot2中的facet_grid()函数,通过密度图和直方图来可视化Year在每个唯一的ALIGN和SEX组合中的分布:
-
在对
SEX应用相同的过滤条件后,构建Year按ALIGN和SEX的密度图:>>> df %>% filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>% ggplot(aes(x=Year)) + geom_density() + facet_grid(ALIGN ~ SEX, labeller = label_both) + facet_grid(ALIGN ~ SEX, labeller = label_both) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), strip.text.x = element_text(size = 30), strip.text.y = element_text(size = 12))执行此命令生成图 5.12,我们在这里使用了
facet_grid()函数创建了六个直方图,列由第一个参数ALIGN分割,行由第二个参数SEX分割。结果显示,对于所有不同的ALIGN和SEX组合,趋势都在上升(制作了更多的电影)。然而,由于Y轴只显示相对密度,我们需要切换到直方图来评估发生的绝对频率。注意,我们使用了strip.text.y属性来调整分面网格标签沿Y轴的文本大小:
图 5.12 – Year按ALIGN和SEX的密度图
-
使用直方图构建相同的图表:
>>> df %>% filter(!(SEX %in% c("Agender Characters", "Genderfluid Characters"))) %>% ggplot(aes(x=Year)) + geom_histogram() + facet_grid(ALIGN ~ SEX, labeller = label_both) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), strip.text.x = element_text(size = 30), strip.text.y = element_text(size = 12))执行此命令生成图 5.13,我们可以看到近年来优秀男女角色的数量稳步上升:
图 5.13 – Year按ALIGN和SEX的直方图
在下一节中,我们将尝试不同的方法来测量数值变量的中心集中度。
测量中心集中度
测量数值变量的集中趋势或中心集中度有不同方法。根据上下文和目的,中心测度通常用来表示一个数值变量的典型观测值。
最流行的中心测度是均值,它是通过计算数字列表的平均值来得到的。换句话说,我们可以通过将所有观测值相加然后除以观测值的数量来获得均值。这可以通过在 R 中使用mean()函数实现。
另一个中心测度是中位数,它是将数字列表从小到大排序后的中间值。这可以通过在 R 中使用median()函数实现。
第三种中心度量是众数,它代表数字列表中最常见的观测值。由于没有内置的函数来计算众数,我们必须编写一个自定义函数,根据出现次数使用table()函数来获取最频繁的观测值。
在决定中心度量之前,观察分布的形状是很重要的。首先,请注意,平均值通常会被拉向偏斜分布的长尾,这是一个从数字列表(如之前的密度图)推断出的连续分布。换句话说,平均值对观测中的极端值很敏感。另一方面,中位数不会受到这种敏感性的影响,因为它只是将有序观测值分成两半的度量。因此,当处理偏斜的连续分布时,中位数是一个更好、更合理的中心度量候选者,除非对极端值(通常被视为异常值)进行了额外的处理。
让我们通过一个练习来看看如何获得三个中心度量。
练习 5.5 – 计算中心度量
在这个练习中,我们将计算APPEARANCES的平均值、中位数和众数,它表示每个角色的出现次数:
-
计算
APPEARANCES的平均值:>>> mean(df$APPEARANCES) NANA结果表明,APPEARANCES的观测值中存在NA值。为了验证这一点,我们可以查看这个连续变量的摘要:>>> summary(df$APPEARANCES) Min. 1st Qu. Median Mean 3rd Qu. Max. NA's 1 1 3 20 9 4043 749的确,存在相当多的
NA值。为了在移除这些NA观测值后计算平均值,我们可以在mean()函数中启用na.rm参数:>>> mean(df$APPEARANCES, na.rm = TRUE) 19.8 -
计算
APPEARANCES的平均值:>>> median(df$APPEARANCES, na.rm = TRUE) 3当平均值和中位数差异很大时,这是一个明显的迹象,表明我们正在处理一个偏斜分布。在这种情况下,
APPEARANCES变量非常偏斜,中位数角色出现三次,最受欢迎的角色出现高达 4,043 次。 -
计算
APPEARANCES的众数:>>> mode <- function(x){ ux <- unique(x) ux[which.max(tabulate(match(x, ux)))] } >>> mode(df$APPEARANCES) 1在这里,我们创建了一个名为
mode()的自定义函数来计算数值变量的众数,其中我们首先使用unique()函数提取一个唯一值的列表,然后使用tabulate()和match()函数计算每个唯一值出现的次数,最后使用which.max()函数获取最大值的索引。结果显示,大多数角色在整个漫威漫画的历史中只出现了一次。现在,让我们详细分析通过
ALIGN的平均值和众数。 -
通过每个
ALIGN级别计算APPEARANCES的平均值和众数:>>> df %>% group_by(ALIGN) %>% summarise(mean_appear = mean(APPEARANCES, na.rm=TRUE), median_appear = median(APPEARANCES, na.rm=TRUE)) ALIGN mean_appear median_appear <fct> <dbl> <dbl> 1 Bad Characters 8.64 3 2 Neutral Characters 20.3 3 3 Good Characters 35.6 5结果显示,好人角色比坏人角色出现得更频繁。
接下来,我们将探讨如何测量连续变量的变异性。
测量变异性
与集中趋势一样,可以使用多个指标来衡量连续变量的变异性或分散性。其中一些对异常值敏感,如方差和标准差,而其他对异常值稳健,如四分位数间距(IQR)。让我们通过一个练习来了解如何计算这些指标。
注意,在箱线图中使用的是稳健的度量,如中位数和 IQR,尽管与给定变量的完整密度相比,隐藏了更多细节。
练习 5.6 - 计算连续变量的变异性
在这个练习中,我们将手动和通过内置函数计算不同的变异度指标。我们将从方差开始,它是通过计算每个原始值与平均值之间的平均平方差来计算的。请注意,这就是总体方差是如何计算的。为了计算样本方差,我们需要调整平均操作,即在方差计算中使用的总观测数减去 1。
此外,方差是原始单位的平方版本,因此不易解释。为了在相同的原始尺度上衡量数据的变异性,我们可以使用标准差,它是通过对方差取平方根来计算的。让我们看看如何在实践中实现这一点:
-
计算移除
NA值后的APPEARANCES的总体方差。保留两位小数:>>> tmp = df$APPEARANCES[!is.na(df$APPEARANCES)] >>> pop_var = sum((tmp - mean(tmp))²)/length(tmp) >>> formatC(pop_var, digits = 2, format = "f") "11534.53"在这里,我们首先从
APPEARANCES中移除NA值,并将结果保存在tmp中。接下来,我们从tmp的每个原始值中减去tmp的平均值,将结果平方,求和所有值,然后除以tmp中的观测数。这本质上遵循方差的定义,即衡量每个观测值相对于中心趋势的平均变异性——换句话说,就是平均值。我们也可以计算样本方差。
-
计算
APPEARANCES的样本方差:>>> sample_var = sum((tmp - mean(tmp))²)/(length(tmp)-1) >>> formatC(sample_var, digits = 2, format = "f") "11535.48"结果现在与总体方差略有不同。请注意,为了计算样本均值,我们在分母中简单地使用一个更少的观测值。这种调整是必要的,尤其是在我们处理有限的样本数据时,尽管随着样本量的增加,差异变得很小。
我们也可以通过调用
var()函数来计算样本方差。 -
使用
var()计算样本方差:>>> formatC(var(tmp), digits = 2, format = "f") "11535.48"结果与我们的先前手动计算的样本方差一致。
为了获得与原始观测值相同的单位的变异性度量,我们可以计算标准差。这可以通过使用
sd()函数来实现。 -
使用
sd()计算标准差:>>> sd(tmp) 107.4变异性的另一个度量是四分位数间距(IQR),它是第三四分位数和第一四分位数之间的差值,并量化了大多数值的范围。
-
使用
IQR()计算四分位数间距:>>> IQR(tmp) 8我们也可以通过调用
summary()函数来验证结果,该函数返回不同的四分位数值:>>> summary(tmp) Min. 1st Qu. Median Mean 3rd Qu. Max. 1.0 1.0 3.0 19.8 9.0 4043.0如前所述,方差和标准差等度量对数据中的极端值敏感,而四分位数范围(IQR)是对异常值稳健的度量。我们可以评估从
tmp中移除最大值后这些度量的变化。 -
从
tmp中移除最大值后的标准差和四分位数范围(IQR):>>> tmp2 = tmp[tmp != max(tmp)] >>> sd(tmp2) 101.04 >>> IQR(tmp2) 8结果显示,移除最大值后 IQR 保持不变,因此比标准差更稳健的度量。
我们还可以通过另一个分类变量的不同级别来计算这些度量。
-
计算每个
ALIGN级别的APPEARANCES的标准差、四分位数范围(IQR)和计数:>>> df %>% group_by(ALIGN) %>% summarise(sd_appear = sd(APPEARANCES, na.rm=TRUE), IQR_appear = IQR(APPEARANCES, na.rm=TRUE), count = n()) ALIGN sd_appear IQR_appear count <fct> <dbl> <dbl> <int> 1 Bad Characters 26.4 5 6334 2 Neutral Characters 112. 8 2094 3 Good Characters 161. 14 4514
接下来,我们将更深入地探讨连续变量分布中的偏度。
处理偏斜分布
除了平均值和标准差之外,我们还可以使用模态和偏度来描述连续变量的分布。模态指的是连续分布中存在的峰的数量。例如,单峰分布,我们迄今为止最常见的形式是钟形曲线,整个分布中只有一个峰值。当有两个峰时,它可以变成双峰分布;当有三个或更多峰时,它变成多峰分布。如果没有可辨别的模态,并且分布在整个支持区域(连续变量的范围)上看起来平坦,则称为均匀分布。图 5.14总结了不同模态的分布:
图 5.14 – 分布中不同类型的模态
另一方面,连续变量可能向左或向右偏斜,或者围绕中心趋势对称。右偏斜分布在其分布的右尾包含更多的极端值,而左偏斜分布在其左侧有长尾。图 5.15说明了分布中不同类型的偏度:
图 5.15 – 分布中的不同类型偏度
分布也可以将它的偏斜归因于连续变量中的异常值。当数据中有多个异常值时,敏感的度量如平均值和方差将变得扭曲,导致分布向异常值偏移。让我们通过一个练习来了解如何处理分布中的偏斜和异常值。
练习 5.7 – 处理偏斜和异常值
在这个练习中,我们将探讨如何处理包含许多极端值,特别是数据中的异常值的偏斜分布:
-
通过
ALIGN可视化APPEARANCES的密度图,对于自 2000 年以来的观测值。设置透明度为0.2:>>> tmp = df %>% filter(Year >= 2000) >>> ggplot(tmp, aes(x=APPEARANCES, fill=ALIGN)) + geom_density(alpha=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.position = c(0.8, 0.8), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20))执行此命令生成图 5.16,其中所有三个分布都相当右偏斜,这是数据中存在许多异常值的明显迹象:
图 5.16 – 由 ALIGN 生成的APPEARANCES密度图
-
移除
APPEARANCES值超过 90 百分位的观测值并生成相同的图表:>>> tmp = tmp %>% filter(APPEARANCES <= quantile(APPEARANCES, 0.9, na.rm=TRUE)) >>> ggplot(tmp, aes(x=log(APPEARANCES), fill=ALIGN)) + geom_density(alpha=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.position = c(0.8, 0.8), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20))执行此命令生成图 5.17,其中所有三个分布都比之前更少地右偏斜。移除异常值是处理极端值的一种方法,尽管移除的观测值中的信息已经丢失。为了控制异常值的影响并同时保留它们的存在,我们可以使用
log()函数对连续变量进行变换,将其转换为对数尺度。让我们看看这在实践中是如何工作的:
图 5.17 – 移除异常值后,由 ALIGN 生成的APPEARANCES密度图
-
对
APPEARANCES应用对数变换并重新生成相同的图表:>>> ggplot(tmp, aes(x=log(APPEARANCES), fill=ALIGN)) + geom_density(alpha=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.position = c(0.8, 0.8), legend.key.size = unit(2, 'cm'), legend.text = element_text(size=20))执行此命令生成图 5.18,其中三个密度图呈现出双峰分布,而不是之前的右偏斜。因此,使用对数函数对连续变量进行变换可以将原始值转换为更受控制的尺度:
图 5.18 – 应用对数变换后,由 ALIGN 生成的APPEARANCES密度图
在下一节中,我们将通过一个案例研究来提高我们在对新的数据集进行 EDA 时的技能。
实践中的 EDA
在本节中,我们将分析一个由 2021 年五大公司股票价格组成的数据库。首先,我们将探讨如何下载和处理这些股票指数,然后进行单变量分析和基于相关性的双变量分析。
获取股票价格数据
要获取特定股票代码的每日股票价格,我们可以使用yfR包从 Yahoo! Finance 下载数据,这是一个包含大量市场和资产金融数据的庞大仓库,在学术界和工业界都得到了广泛的应用。以下练习说明了如何使用yfR下载股票数据。
练习 5.8 – 下载股票价格
在这个练习中,我们将探讨如何指定不同的参数,以便我们可以从 Yahoo! Finance 下载股票价格,包括股票代码和日期范围:
-
安装并加载
yfR包:>>> install.packages("yfR") >>> library(yfR)注意,在
install.packages()函数中,我们需要将包名用一对双引号括起来。 -
指定起始日期和结束日期参数,以及股票代码,以确保它们覆盖 Facebook(现在为
META)、Netflix(NFLX)、Google(GOOG)、Amazon(AMZN)和 Microsoft(MSFT):>>> first_date = as.Date("2021-01-01") >>> last_date = as.Date("2022-01-01") >>> my_ticker <- c('META', 'NFLX', 'GOOG', 'AMZN', 'MSFT')这里,起始日期和结束日期格式化为
Date类型,股票名称连接成一个向量。 -
使用
yf_get()函数下载股票价格并将结果存储在df中:>>> df <- yf_get(tickers = my_ticker, first_date = first_date, last_date = last_date)执行此命令会生成以下消息,显示已成功下载所有五只股票的数据。由于一年中有 252 个交易日,每只股票在 2021 年有 252 行数据:
── Running yfR for 5 stocks | 2021-01-01 --> 2022-01-01 (365 days) ── ℹ Downloading data for benchmark ticker ^GSPC ℹ (1/5) Fetching data for AMZN - found cache file (2021-01-04 --> 2021-12-31) - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- Got it! ℹ (2/5) Fetching data for GOOG - found cache file (2021-01-04 --> 2021-12-31) - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- Good stuff! ℹ (3/5) Fetching data for META ! - not cached - cache saved successfully - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- Mais contente que cusco de cozinheira! ℹ (4/5) Fetching data for MSFT - found cache file (2021-01-04 --> 2021-12-31) - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- All OK! ℹ (5/5) Fetching data for NFLX - found cache file (2021-01-04 --> 2021-12-31) - got 252 valid rows (2021-01-04 --> 2021-12-31) - got 100% of valid prices -- Youre doing good! ℹ Binding price data ── Diagnostics ─────────────────────────────────────── Returned dataframe with 1260 rows -- Time for some tea? ℹ Using 156.6 kB at /var/folders/zf/d5cczq0571n0_x7_7rdn0r640000gn/T//Rtmp7hl9eR/yf_cache for 1 cache files ℹ Out of 5 requested tickers, you got 5 (100%)让我们检查数据集的结构:
>>> str(df) tibble [1,260 × 11] (S3: tbl_df/tbl/data.frame) $ ticker : chr [1:1260] "AMZN" "AMZN" "AMZN" "AMZN" ... $ ref_date : Date[1:1260], format: "2021-01-04" ... $ price_open : num [1:1260] 164 158 157 158 159 ... $ price_high : num [1:1260] 164 161 160 160 160 ... $ price_low : num [1:1260] 157 158 157 158 157 ... $ price_close : num [1:1260] 159 161 157 158 159 ... $ volume : num [1:1260] 88228000 53110000 87896000 70290000 70754000 ... $ price_adjusted : num [1:1260] 159 161 157 158 159 ... $ ret_adjusted_prices : num [1:1260] NA 0.01 -0.0249 0.00758 0.0065 ... $ ret_closing_prices : num [1:1260] NA 0.01 -0.0249 0.00758 0.0065 ... $ cumret_adjusted_prices: num [1:1260] 1 1.01 0.985 0.992 0.999 ... - attr(*, "df_control")= tibble [5 × 5] (S3: tbl_df/tbl/data.frame) ..$ ticker : chr [1:5] "AMZN" "GOOG" "META" "MSFT" ... ..$ dl_status : chr [1:5] „OK" „OK" „OK" „OK" ... ..$ n_rows : int [1:5] 252 252 252 252 252 ..$ perc_benchmark_dates: num [1:5] 1 1 1 1 1 ..$ threshold_decision : chr [1:5] "KEEP" "KEEP" "KEEP" "KEEP" ...下载的数据包括每日开盘价、收盘价、最高价和最低价等信息。
在以下章节中,我们将使用调整后的价格字段 price_adjusted,该字段已调整公司事件,如拆股、股息等。通常,我们在分析股票时使用它,因为它代表了股东的实际财务表现。
单变量分析个别股票价格
在本节中,我们将基于股票价格进行图形分析。由于股票价格是数值型的时间序列数据,我们将使用直方图、密度图和箱线图等图表进行可视化。
练习 5.9 – 下载股票价格
在本练习中,我们将从五只股票的时间序列图开始,然后生成适合连续变量的其他类型图表:
-
为五只股票生成时间序列图:
>>> ggplot(df, aes(x = ref_date, y = price_adjusted, color = ticker)) + geom_line() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令生成 图 5.19,其中 Netflix 在股票价值方面领先。然而,它也遭受了巨大的波动,尤其是在 2021 年 11 月左右:
图 5.19 – 五只股票的时间序列图
-
为五只股票中的每一只生成一个直方图,每个直方图有 100 个区间:
>>> ggplot(df, aes(x=price_adjusted, fill=ticker)) + geom_histogram(bins=100) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令生成 图 5.20,显示 Netflix 在股票价值方面具有最大的平均值和方差。Google 和 Amazon 似乎具有相似的分布,Facebook 和 Microsoft 也是如此:
图 5.20 – 五只股票的直方图
-
为五只股票中的每一只生成一个密度图。将透明度设置为
0.2:>>> ggplot(df, aes(x=price_adjusted, fill=ticker)) + geom_density(alpha=0.2) + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令生成 图 5.21,与直方图相比,这些图现在在视觉上更清晰:
图 5.21 – 五只股票的密度图
-
为五只股票中的每一只生成一个箱线图:
>>> ggplot(df, aes(ticker, price_adjusted, fill=ticker)) + geom_boxplot() + theme(axis.text=element_text(size=18), axis.title=element_text(size=18,face="bold"), legend.text = element_text(size=20))执行此命令生成 图 5.22。箱线图擅长显示每只股票的中心趋势和变异。例如,Netflix 在所有五只股票中具有最大的平均值和方差:
图 5.22 – 五只股票的箱线图
-
获取每只股票的平均值、标准差、四分位数范围和计数:
>>> df %>% group_by(ticker) %>% summarise(mean = mean(price_adjusted, na.rm=TRUE), sd = sd(price_adjusted, na.rm=TRUE), IQR = IQR(price_adjusted, na.rm=TRUE), count = n()) # A tibble: 5 × 5 ticker mean sd IQR count <chr> <dbl> <dbl> <dbl> <int> 1 AMZN 167. 8.00 10.7 252 2 GOOG 126\. 18.4 31.1 252 3 META 321\. 34.9 44.2 252 4 MSFT 273\. 37.2 58.5 252 5 NFLX 558\. 56.0 87.5 252
在下一节中,我们将查看每对股票之间的成对相关性。
相关性分析
相关性衡量两个变量之间协变的强度。有几种方法可以计算相关性的具体值,其中皮尔逊相关是最广泛使用的。皮尔逊相关是一个介于 -1 到 1 之间的值,其中 1 表示两个完全且正相关的变量,-1 表示完美的负相关性。完美的相关性意味着一个变量的值的变化总是与另一个变量的值的变化成比例。例如,当 y = 2x 时,变量 x 和 y 之间的相关性为 1,因为 y 总是正比于 x 而变化。
我们不必手动计算所有变量之间的成对相关性,可以使用 corrplot 包自动计算和可视化成对相关性。让我们通过一个练习来看看如何实现这一点。
练习 5.10 – 下载股票价格
在这个练习中,我们首先将之前的 DataFrame 从长格式转换为宽格式,以便每个股票都有一个单独的列,表示不同日期/行之间的调整价格。然后,将使用宽格式数据集生成成对相关性图:
-
使用
tidyr包中的spread()函数将之前的数据集转换为宽格式,并将结果保存在wide_df中:>>> library(tidyr) >>> wide_df <- df %>% select(ref_date, ticker, price_adjusted) %>% spread(ticker, price_adjusted)在这里,我们首先选择三个变量,其中
ref_date作为行级日期索引,ticker的唯一值作为要分散在 DataFrame 中的列,price_adjusted用于填充宽 DataFrame 的单元格。有了这些,我们可以检查新数据集的前几行:>>> head(wide_df) # A tibble: 6 × 6 ref_date AMZN GOOG META MSFT NFLX <date> <dbl> <dbl> <dbl> <dbl> <dbl> 1 2021-01-04 159. 86.4 269. 214. 523. 2 2021-01-05 161. 87.0 271. 215. 521. 3 2021-01-06 157. 86.8 263. 209. 500. 4 2021-01-07 158. 89.4 269. 215. 509. 5 2021-01-08 159. 90.4 268. 216. 510. 6 2021-01-11 156. 88.3 257. 214. 499.现在,DataFrame 已经从长格式转换为宽格式,这将有助于稍后创建相关性图。
-
使用
corrplot包中的corrplot()函数生成相关性图(如果尚未安装,请先安装):>>> install.packages("corrplot") >>> library(corrplot) >>> cor_table = cor(wide_df[,-1]) >>> corrplot(cor_table, method = "circle")执行这些命令会生成 图 5*.23*。每个圆圈代表对应股票之间相关性的强度,其中更大、更暗的圆圈表示更强的相关性:
图 5.23 – 每对股票之间的相关性图
注意,相关性图依赖于 cor_table 变量,该变量存储成对相关性作为表格,如下所示:
>>> cor_table
AMZN GOOG META MSFT NFLX
AMZN 1.000 0.655 0.655 0.635 0.402
GOOG 0.655 1.000 0.855 0.945 0.633
META 0.655 0.855 1.000 0.692 0.267
MSFT 0.635 0.945 0.692 1.000 0.782
NFLX 0.402 0.633 0.267 0.782 1.000
变量之间的高度相关性可能好也可能不好。当需要预测的因变量(也称为目标结果)与自变量(也称为预测变量、特征或协变量)高度相关时,我们更愿意将这个特征包含在预测模型中,因为它与目标变量的协变很高。另一方面,当两个特征高度相关时,我们倾向于忽略其中一个并选择另一个,或者应用某种正则化和特征选择方法来减少相关特征的影响。
摘要
在本章中,我们介绍了进行 EDA 的基本技术。我们首先回顾了分析和管理分类数据的常见方法,包括频率计数和条形图。然后,当我们处理多个分类变量时,我们介绍了边缘分布和分面条形图。
接下来,我们转向分析数值变量,并涵盖了敏感度量,如集中趋势(均值)和变异(方差),以及稳健度量,如中位数和四分位数间距。有几种图表可用于可视化数值变量,包括直方图、密度图和箱线图,所有这些都可以与另一个分类变量结合使用。
最后,我们通过使用股票价格数据进行了案例研究。我们首先从 Yahoo! Finance 下载了真实数据,并应用所有 EDA 技术来分析数据,然后创建了一个相关性图来指示每对变量之间协变的强度。这使我们能够对变量之间的关系有一个有帮助的理解,并启动预测建模阶段。
在下一章中,我们将介绍 r markdown,这是一个广泛使用的 R 包,用于生成交互式报告。