R-应用监督学习-一-

17 阅读14分钟

R 应用监督学习(一)

原文:annas-archive.org/md5/a69bdbe06296bcd4cedce10b02fa8fe7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

关于

本节简要介绍了作者、本书涵盖的内容、您开始所需的技术技能,以及完成所有包含的活动和练习所需的硬件和软件要求。

关于本书

R 是最早为统计计算和数据分析开发的编程语言之一,它对可视化的支持非常出色。随着数据科学的兴起,R 在众多数据科学从业者中成为无疑是一个很好的编程语言选择。由于 R 是开源的,并且在构建复杂的统计模型方面极其强大,它很快在工业和学术界得到了广泛应用。

使用 R 进行应用监督学习 覆盖了使用 R 开发满足您商业需求的监督机器学习算法应用的完整过程。您的学习曲线从发展您的分析思维开始,使用业务输入或领域研究创建问题陈述。您将了解许多比较各种算法的评估指标,然后您将使用这些指标来选择最适合您问题的算法。在确定您想要使用的算法后,您将学习超参数优化技术来微调您的最优参数集。为了避免模型过拟合,您还将了解到如何添加各种正则化项。您还将学习如何将您的模型部署到生产环境中。

当你完成这本书后,你将成为一位在建模监督机器学习算法方面的高手,能够精确满足你的商业需求。

关于作者

Karthik Ramasubramanian 在印度的 PSG 工程技术学院完成了理论计算机科学的硕士学位,在那里他在计算机和网络安全的研究工作中开创了机器学习、数据挖掘和模糊逻辑的应用。他在零售、快速消费品、电子商务、信息技术和酒店业的多国公司和独角兽初创公司中,领导数据科学和商业分析已有超过七年的经验。

他是一位研究人员和问题解决者,拥有丰富的数据科学生命周期经验,从数据问题发现到为各种行业用例创建数据科学概念验证和产品。在他的领导角色中,Karthik 通过数据科学解决方案解决了许多以投资回报率为驱动的商业问题。他通过各种在线平台和大学参与项目,在全球范围内指导并培训了数百名专业人员和学生在数据科学方面的知识。他还开发了基于深度学习模型的智能聊天机器人,这些模型能够理解类似人类的交互、客户细分模型、推荐系统和许多自然语言处理模型。

他还是 Apress 出版社(Springer Business+Science Media 的出版社)出版的《使用 R 进行机器学习》一书的作者。本书取得了巨大成功,在线下载量超过 50,000 次,精装版销量也颇高。本书随后出版了第二版,增加了关于深度学习和时间序列建模的扩展章节。

Jojo Moolayil是一位拥有超过六年工业经验的、在人工智能、深度学习、机器学习和决策科学领域的专业人士。他是 Apress 出版的《学习 Keras 进行深度神经网络》和 Packt Publishing 出版的《更明智的决策——物联网与决策科学的交汇》的作者。他与多个行业领导者合作,在多个垂直领域开展了具有重大影响力和关键性的数据科学和机器学习项目。他目前在加拿大亚马逊网络服务公司担任研究科学家。

除了撰写关于人工智能、决策科学和物联网的书籍外,Jojo 还担任了 Apress 和 Packt Publishing 出版的同一领域各种书籍的技术审稿人。他是一位活跃的数据科学导师,并在 blog.jojomoolayil.com 维护着一个博客。

学习目标

  • 培养分析思维,精确地识别商业问题

  • 使用 dplyr、tidyr 和 reshape2 整理数据

  • 使用 ggplot2 可视化数据

  • 使用 k 折算法验证您的监督机器学习模型

  • 使用网格搜索、随机搜索和贝叶斯优化优化超参数

  • 使用 Plumber 在 AWS Lambda 上部署您的模型

  • 通过特征选择和降维提高模型性能

读者对象

本书专为希望探索监督机器学习各种方法和其各种用例的新手和中级数据分析师、数据科学家和数据工程师设计。在统计学、概率论、微积分、线性代数和编程方面的一些背景知识将有助于您彻底理解和跟随本书的内容。

方法

*《使用 R 进行应用监督学习》*完美地平衡了理论与练习。每个模块都旨在在前一个模块的学习基础上进行构建。本书包含多个活动,使用现实生活中的商业场景供您练习,并在高度相关的环境中应用您的新技能。

最小硬件要求

为了获得最佳的学生体验,我们推荐以下硬件配置:

  • 处理器:Intel 或 AMD 4 核或更好

  • 内存:8 GB RAM

  • 存储:20 GB 可用空间

软件要求

您需要提前安装以下软件:

  • 操作系统:Windows 7、8.1 或 10、Ubuntu 14.04 或更高版本、或 macOS Sierra 或更高版本

  • 浏览器:Google Chrome 或 Mozilla Firefox

  • RStudio

  • RStudio Cloud

您还需要提前安装以下软件、包和库:

  • dplyr

  • tidyr

  • reshape2

  • lubridate

  • ggplot2

  • caret

  • mlr

  • OpenML

习惯用法

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:"LocationWindDirRainToday 变量以及许多其他变量是分类的,其余的是连续的。"

代码块设置如下:

temp_df<-as.data.frame(
  sort(
  round(
  sapply(df, function(y) sum(length(which(is.na(y)))))/dim(df)[1],2)
  )
)
colnames(temp_df) <- "NullPerc"

新术语和重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"点击 下一步 按钮,并导航到 详细信息 页面。"

安装和设置

要在 RStudio Cloud 上安装一个包,你可以使用以下语法:

install.packages("Package_Name")

例如:

install.packages("ggplot2")

要验证安装,请运行以下命令:

library(Package_Name)

例如:

library(ggplot2)

安装代码包

将课程代码包复制到 C:/Code 文件夹。

其他资源

该书的代码包也托管在 GitHub 上:github.com/TrainingByP….

我们还有来自我们丰富的书籍、视频和 E-learning 产品目录中的其他代码包,可在 github.com/PacktPublis…github.com/TrainingByP…. 查看它们!

第一章:R 高级分析

学习目标

到本章结束时,你将能够:

  • 解释高级 R 编程结构

  • 打印真实世界数据集的摘要统计信息

  • 从 CSV、文本和 JSON 文件中读取数据

  • 编写 R markdown 文件以实现代码可重复性

  • 解释 R 数据结构,如 data.frame、data.table、列表、数组以及矩阵

  • 实现 cbind、rbind、merge、reshape、aggregate 和 apply 函数

  • 使用 dplyr、plyr、caret、tm 等包以及其他许多包

  • 使用 ggplot 创建可视化

在本章中,我们将为使用 R 进行编程奠定基础,并了解高级分析的各种语法和数据结构。

简介

R 是早期为统计计算和数据分析开发的编程语言之一,具有良好的可视化支持。随着数据科学的兴起,R 在众多数据科学从业者中成为无可争议的编程语言选择。由于 R 是开源的,并且在构建复杂的统计模型方面非常强大,它很快在工业和学术界得到了广泛应用。

工具和软件如 SAS 和 SPSS 仅大型企业能够负担得起,而传统的编程语言如 C/C++和 Java 不适合进行复杂的数据分析和构建模型。因此,需要一个更加直接、全面、社区驱动、跨平台兼容和灵活的编程语言成为了一种必需。

尽管 Python 编程语言近年来因其在整个行业的采用和强大的生产级实现而越来越受欢迎,但 R 仍然是快速原型化高级机器学习模型的编程语言选择。R 拥有最丰富的包集合(一个用于完成复杂过程的函数/方法的集合,否则需要花费大量时间和精力来实现)。在撰写本书时,综合 R 档案网络CRAN),一个全球范围内的 FTP 和 Web 服务器网络,存储了 R 的代码和文档的相同、最新版本,拥有超过 13,000 个包。

虽然有许多关于学习 R 基础知识的书籍和在线资源,但在本章中,我们将仅限于涵盖在许多数据科学项目中广泛使用的 R 编程的重要主题。我们将使用来自 UCI 机器学习仓库的真实世界数据集来展示这些概念。本章的材料将对新接触 R 编程的学习者很有用。在接下来的章节中,监督学习概念将借鉴本章的许多实现。

与真实世界数据集一起工作

目前网上有大量可用的开放数据集。以下是一些流行的开放数据集来源:

  • Kaggle: 一个用于举办数据科学竞赛的平台。官方网站是www.kaggle.com/

  • UCI 机器学习仓库: 机器学习社区用于实证分析机器学习算法的数据库、领域理论和数据生成器的集合。您可以通过导航到archive.ics.uci.edu/ml/index.php URL 访问官方网站。

  • data.gov.in: 开放印度政府数据平台,可在data.gov.in/找到。

  • 世界银行开放数据: 免费和开放访问全球发展数据,可通过data.worldbank.org/访问。

越来越多的私营和公共组织愿意将他们的数据公开供公众访问。然而,这仅限于那些组织正在通过 Kaggle 等众包平台寻求解决其数据科学问题的复杂数据集。从组织内部获得的数据作为工作的一部分进行学习是没有替代品的,这项工作在处理和分析方面提供了各种挑战。

数据处理方面的重大学习机会和挑战也来自公共数据源,因为这些数据源中的数据并不都是干净和标准格式的。除了 CSV 之外,JSON、Excel 和 XML 也是一些其他格式,尽管 CSV 占主导地位。每种格式都需要单独的编码和解码方法,因此在 R 中需要一个单独的读取器包。在我们下一节中,我们将讨论各种数据格式以及如何详细处理可用的数据。

在本章以及许多其他章节中,我们将使用来自 UCI 机器学习仓库的葡萄牙银行机构的直接营销活动(电话)数据集。(archive.ics.uci.edu/ml/datasets/bank+marketing)以下表格详细描述了字段:

![图 1.1:来自 UCI 机器学习仓库的葡萄牙银行机构数据集(第一部分)]

](github.com/OpenDocCN/f…)

图 1.1:来自 UCI 机器学习仓库的葡萄牙银行机构数据集(第一部分)

图 1.2:来自 UCI 机器学习仓库的葡萄牙银行机构数据集(第二部分)

图 1.2:来自 UCI 机器学习仓库的葡萄牙银行机构数据集(第二部分)

在以下练习中,我们将下载bank.zip数据集作为 ZIP 文件,并使用unzip方法解压它。

练习 1:使用 unzip 方法解压下载的文件

在这个练习中,我们将编写一个 R 脚本,从 UCI 机器学习仓库下载葡萄牙银行直接营销活动数据集,并使用unzip函数在指定文件夹中提取 ZIP 文件的内容。

执行以下步骤以完成练习:

  1. 首先,在您的系统上打开 R Studio。

  2. 现在,使用以下命令设置您选择的当前工作目录:

    wd <- "<WORKING DIRECTORY>"
    setwd(wd)
    
    注意

    本书中的 R 代码使用 R 版本 3.2.2 实现。

  3. 使用download.file()方法下载包含数据集的 ZIP 文件:

    url <- "https://archive.ics.uci.edu/ml/machine-learning-databases/00222/bank.zip"
    destinationFileName <- "bank.zip"
    download.file(url, destinationFileName,method = "auto", quiet=FALSE)
    
  4. 现在,在我们使用unzip()方法在工作目录中解压文件之前,我们需要选择一个文件并将其文件路径保存在 R 中(对于 Windows)或指定完整路径:

    zipFile<-file.choose()
    
  5. 定义 ZIP 文件解压的文件夹:

    outputDir <- wd
    
  6. 最后,使用以下命令解压 ZIP 文件:

    unzip(zipFile, exdir=outputDir)
    

    输出如下:

图 1.3:解压 bank.zip 文件

图 1.3:解压 bank.zip 文件

从各种数据格式读取数据

来自数字系统的数据以各种形式生成:电子商务网站上的浏览历史、点击流数据、客户的购买历史、社交媒体互动、零售店的客流量、卫星和无人机图像,以及众多其他格式和类型的数据。我们正处于一个令人兴奋的时代,技术正在极大地改变我们的生活,企业正在利用它来制定他们的下一份数据战略,以做出更好的决策。

能够收集大量不同类型的数据是不够的;我们还需要从中提取价值。一天中拍摄的视频监控可以帮助政府执法团队改善公共场所的实时监控。挑战在于如何在单一系统中处理大量异构数据格式。

客户关系管理(CRM)应用中的交易数据大多为表格形式,而流入社交媒体的数据大多是文本、音频、视频和图片。

我们可以将数据格式分为结构化——如 CSV 和数据库表之类的表格数据;非结构化——如推文、FB 帖子和 Word 文档之类的文本数据;以及半结构化。与难以让机器处理和理解的文本不同,半结构化提供了关联的元数据,这使得计算机处理它变得容易。它广泛应用于许多 Web 应用程序中的数据交换,JSON 是半结构化数据格式的一个例子。

在本节中,我们将了解如何在 R 中加载、处理和转换各种数据格式。在本书的范围内,我们将处理 CSV、文本和 JSON 数据。

CSV 文件

CSV 文件是结构化数据最常见的数据存储和交换格式。R 提供了一个名为read.csv()的方法,用于从 CSV 文件中读取数据。它将数据读入data.frame(更多内容将在下一节介绍)。该方法接受许多参数;其中两个必需参数是filename的路径和sep,它指定了分隔列值的字符。summary()方法描述了六个汇总统计量:最小值第一四分位数中位数平均值第三四分位数最大值

在以下练习中,我们将读取 CSV 文件并总结其列。

练习 2:读取 CSV 文件并总结其列

在这个练习中,我们将读取之前提取的 CSV 文件,并使用 summary 函数打印数值变量的最小值、最大值、平均值、中位数、第一四分位数和第三四分位数,并计算分类变量的类别数。

执行以下步骤以读取 CSV 文件并随后总结其列:

  1. 首先,使用 read.csv 方法将 bank-full.csv 载入 DataFrame:

    df_bank_detail <- read.csv("bank-full.csv", sep = ';')
    
  2. 打印 DataFrame 的摘要:

    summary(df_bank_detail)
    

    输出如下:

    ##       age                 job           marital          education    
    ##  Min.   :18.00   blue-collar:9732   divorced: 5207   primary  : 6851  
    ##  1st Qu.:33.00   management :9458   married :27214   secondary:23202  
    ##  Median :39.00   technician :7597   single  :12790   tertiary :13301  
    ##  Mean   :40.94   admin.     :5171                    unknown  : 1857  
    ##  3rd Qu.:48.00   services   :4154                                     
    ##  Max.   :95.00   retired    :2264                                     
    

JSON

JSON 是用于共享和存储数据的下一个最常用的数据格式。它与 CSV 文件不同,CSV 文件只处理行和列的数据,其中每一行都有确定数量的列。例如,在电子商务客户数据中,每一行可能代表一个客户,其信息存储在单独的列中。对于一个客户,如果某列没有值,该字段存储为 NULL。

JSON 提供了额外的灵活性,即每个客户可以有可变数量的字段。这种类型的灵活性减轻了开发者在传统关系数据库中维护模式时的负担,在传统关系数据库中,相同客户的数据可能分散在多个表中以优化存储和查询时间。

JSON 更像是一种键值存储类型的存储,我们只关心键(如姓名、年龄和出生日期)及其对应的值。虽然这听起来很灵活,但必须采取适当的注意,否则管理可能会在某些时候失控。幸运的是,随着近年来大数据技术的出现,许多文档存储(键值存储的一个子类),通常也称为 NoSQL 数据库,可用于存储、检索和处理此类格式的数据。

在以下练习中,JSON 文件包含 2015-16 年印度泰米尔纳德邦辣椒(香料和调味品)种植区的数据。键包括 面积(公顷)、产量(公担)和 生产力(每公顷平均产量)。

jsonlite 包提供了一个实现,可以将 JSON 文件读取并转换为 DataFrame,这使得分析更加简单。fromJSON 方法读取 JSON 文件,如果 fromJSON 函数中的 flatten 参数设置为 TRUE,则返回一个 DataFrame。

练习 3:读取 JSON 文件并将数据存储在 DataFrame 中

在这个练习中,我们将读取 JSON 文件并将数据存储在 DataFrame 中。

执行以下步骤以完成练习:

  1. data.gov.in/catalog/area-production-productivity-spices-condiments-district-wise-tamil-nadu-year-2015-16 下载数据。

  2. 首先,使用以下命令安装读取 JSON 文件所需的包:

    install jsonlite package
    install.packages("jsonlite")
    library(jsonlite)
    
  3. 接下来,使用 fromJSON 方法读取 JSON 文件,如下所示:

    json_file <- "crop.json"
    json_data <- jsonlite::fromJSON(json_file, flatten = TRUE)
    
  4. 列表中的第二个元素包含包含作物生产价值的 DataFrame。从 json_data 中检索它,并将其存储为名为 crop_production 的 DataFrame:

    crop_production <- data.frame(json_data[[2]])
    
  5. 接下来,使用以下命令重命名列:

    colnames(crop_production) <- c("S.No","District","Area","Production","PTY")
    
  6. 现在,使用 head() 函数打印前六行:

    head(crop_production)
    

    输出如下:

    ##   S.No   District Area Production  PTY
    ## 1    1   Ariyalur   NA         NA   NA
    ## 2    2 Coimbatore  808         26 0.03
    ## 3    3  Cuddalore   NA         NA   NA
    ## 4    4 Dharmapuri   NA         NA   NA
    ## 5    5   Dindigul  231          2 0.01
    ## 6    6      Erode   NA         NA   NA
    

文本

非结构化数据是网络的通用语言。所有社交媒体、博客、网页以及许多其他信息来源都是文本形式且杂乱无章,难以从中提取任何有意义的信息。越来越多的研究工作来自 自然语言处理NLP)领域,其中计算机不仅变得更擅长理解单词的意义,而且还能理解句子中单词的使用上下文。计算机聊天机器人的兴起,它能对人类查询做出响应,是理解文本信息最复杂的形式。

在 R 中,我们将使用 tm 文本挖掘包来展示如何读取、处理和从文本数据中检索有意义的信息。我们将使用 Kaggle 上的 Amazon Food Review 数据集的小样本(www.kaggle.com/snap/amazon-fine-food-reviews)来练习本节的内容。

tm 包中,文本文档的集合被称为 tm 包的 VCorpusVCorpus 对象)。我们可以使用 inspect() 方法。以下练习使用 lapply 方法遍历前两条评论并将文本转换为字符。你将在 函数的 Apply 家族 部分了解更多关于 apply 家族函数的信息。

练习 4:读取具有文本列的 CSV 文件并将数据存储在 VCorpus 中

在这个练习中,我们将读取具有文本列的 CSV 文件并将数据存储在 VCorpus 中。

完成练习的步骤如下:

  1. 首先,让我们将 R 中的文本挖掘包加载到系统中以读取文本文件:

    library(tm)
    
  2. 现在,从文件中读取前 10 条评论:

    review_top_10 <- read.csv("Reviews_Only_Top_10_Records.csv")
    
  3. 要将文本列存储在 VCorpus 中,使用以下命令:

    review_corpus <- VCorpus(VectorSource(review_top_10$Text))
    
  4. 要检查前两条评论的结构,执行以下命令:

    inspect(review_corpus[1:2])
    

    输出如下:

    ## <<VCorpus>>
    ## Metadata:  corpus specific: 0, document level (indexed): 0
    ## Content:  documents: 2
    ## [[1]]
    ## <<PlainTextDocument>>
    ## Metadata:  7
    ## Content:  chars: 263
    ## [[2]]
    ## <<PlainTextDocument>>
    ## Metadata:  7
    ## Content:  chars: 190
    
  5. 使用 lapply 将第一条评论转换为字符并打印:

    lapply(review_corpus[1:2], as.character)
    ## $'1'
    ## [1] "I have bought several of the Vitality canned dog food products and have found them all to be of good quality. The product looks more like a stew than a processed meat and it smells better. My Labrador is finicky and she appreciates this product better than  most."
    ## $'2'
    ## [1] "Product arrived labeled as Jumbo Salted Peanuts...the peanuts were actually small sized unsalted. Not sure if this was an error or if the vendor intended to represent the product as \"Jumbo\".
    

我们将在稍后的部分重新访问 review_corpus 数据集,展示如何将非结构化文本信息转换为结构化表格数据。

除了 CSV、文本和 JSON 格式之外,还有许多其他数据格式,这取决于数据来源及其用途。R 拥有一系列丰富的库,可以帮助处理许多格式。R 可以导入不仅包括标准格式(除前三种之外)如 HTML 表格和 XML,还可以导入特定于分析工具的格式,如 SAS 和 SPSS。这种民主化导致了行业专家的重大迁移,他们之前在专有工具(成本高昂且通常只有大型企业才有)中工作,转向开源分析编程语言,如 R 和 Python。

为代码可重复性编写 R Markdown 文件

分析学的巨大成功是信息与知识网络在主题周围开始传播的结果。出现了更多的开源社区,开发者们愉快地与外界分享他们的工作,许多数据项目变得可重复。这种变化意味着一个人开始的工作很快就会被一个社区以许多不同的形式适应、即兴创作和修改,在它被完全不同的领域采用之前,这个领域与它最初出现的领域完全不同。想象一下,每个在会议上发表的研究工作都提交了一个易于重复的代码和数据集,以及他们的研究论文。这种变化正在加快想法与现实相遇的速度,创新将开始蓬勃发展。

现在,让我们看看如何在单个文件中创建这样的可重复工作,我们称之为R Markdown文件。在以下活动中,我们将演示如何在 RStudio 中创建一个新的 R Markdown 文件。有关 R Markdown 的详细介绍,请参阅rmarkdown.rstudio.com/lesson-1.html

在下一个活动中,你将把 练习 4 中显示的代码(使用 Text Column 读取 CSV 文件并将数据存储在 VCorpus 中)重新创建到 R Markdown 中。在 图 4.2 中观察,你刚刚在 R Markdown 中编写了说明和代码,当执行 Knit to Word 操作时,它将说明、代码及其输出整齐地交织到一个文档中。

活动 1:创建一个 R Markdown 文件来读取 CSV 文件并编写数据摘要

在这个活动中,我们将创建一个 R Markdown 文件来读取 CSV 文件,并在一个文档中打印数据的小型摘要:

执行以下步骤以完成活动:

  1. 打开 RStudio 并导航到R Markdown选项:图 1.4:在 RStudio 中创建新的 R Markdown 文件

    图 1.4:在 RStudio 中创建新的 R Markdown 文件
  2. 为文档提供标题作者名称,并将默认输出格式选择为Word图 1.5:使用 read.csv 方法读取数据

    图 1.5:使用 read.csv 方法读取数据
  3. 使用 read.csv() 方法读取 bank-full.csv 文件。

  4. 最后,使用summary方法将摘要打印到 word 文件中。

    输出如下:

图 1.6:使用 summary 方法后的最终输出

图 1.6:使用 summary 方法后的最终输出
注意

该活动的解决方案可以在第 438 页找到。

R 中的数据结构

在任何编程语言中,数据结构是存储信息并使其准备进一步处理的基本单元。根据数据类型,有各种形式的数据结构可用于存储处理。下一节中解释的每个数据结构都有其特征和适用性。

在本节中,我们将探索每个数据结构及其如何与我们的数据一起使用。

向量

c()方法,如下所示:

c_names <- c("S.No","District","Area","Production","PTY")

我们可以通过指定向量名称旁边的方括号中的索引来提取向量中的第二个值。让我们回顾以下代码,其中我们提取第二个索引的值:

c_names[2]

输出如下:

## [1] "District"

使用c()方法连接的字符串集合是一个向量。它可以存储同质的数据集合,如字符、整数或浮点值。在尝试用字符存储整数时,将发生隐式类型转换,将所有值转换为字符。

谨慎

注意,这不一定每次都是预期的行为。需要谨慎,尤其是在数据不干净时。否则可能会造成比通常编程错误更难找到的错误。

矩阵

矩阵是用于存储n维数据的更高维数据结构。它适用于存储表格数据。与向量类似,矩阵也只允许在其行和列中存储同质的数据集合。

以下代码生成 16 个来自参数为试验次数(size) = 100和成功概率等于0.4的二项分布的随机数。R 中的rbinom()方法用于生成此类随机数:

r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)

现在,要存储r_number为矩阵,请使用以下命令:

matrix(r_numbers, nrow = 4, ncol = 4)

输出如下:

##      [,1] [,2] [,3] [,4]
## [1,]   48   39   37   39
## [2,]   34   41   32   38
## [3,]   40   34   42   46
## [4,]   37   42   36   44

让我们扩展我们在练习 4中使用的文本挖掘示例,即使用 VCorpus 存储具有文本列的 CSV 文件,以了解矩阵在文本挖掘中的使用。

考虑以下两个评论。使用lapply将第一个评论转换为as.character并打印:

lapply(review_corpus[1:2], as.character)

输出如下:

## $'1'
## [1] "I have bought several of the Vitality canned dog food products and have found them all to be of good quality. The product looks more like a stew than a processed meat, and it smells better. My Labrador is finicky, and she appreciates this product better than  most."
## $'2'
## [1] "Product arrived labeled as Jumbo Salted Peanuts...the peanuts were actually small sized unsalted. Not sure if this was an error or if the vendor intended to represent the product as \"Jumbo\".

现在,在以下练习中,我们将转换数据以从这两段文本中删除停用词、空白和标点符号。然后我们将进行词干提取(both lookinglooked 将被缩减为 look)。此外,为了保持一致性,将所有文本转换为小写。

练习 5:对数据进行转换以便进行分析

在这个练习中,我们将对数据进行转换,使其可用于进一步分析。

完成练习的以下步骤:

  1. 首先,使用以下命令将数据中的所有字符转换为小写:

    top_2_reviews <- review_corpus[1:2]
    top_2_reviews <- tm_map(top_2_reviews,content_transformer(tolower))
    lapply(top_2_reviews[1], as.character)
    

    输出结果如下:

    ## [1] "I have bought several of the vitality canned dog food products and have found them all to be of good quality. the product looks more like a stew than a processed meat and it smells better. my labrador is finicky, and she appreciates this product better than  most."
    
  2. 接下来,从数据中移除停用词,例如,athean 以及更多:

    top_2_reviews <- tm_map(top_2_reviews,removeWords, stopwords("english"))
    lapply(top_2_reviews[1], as.character)
    

    输出结果如下:

    ## [1] "  bought several   vitality canned dog food products   found      good quality.  product looks  like  stew   processed meat   smells better.  labrador  finicky   appreciates  product better   ."
    
  3. 使用以下命令移除单词之间的额外空格:

    top_2_reviews <- tm_map(top_2_reviews,stripWhitespace)
    lapply(top_2_reviews[1], as.character)
    

    输出结果如下:

    ## [1] " bought several vitality canned dog food products found good quality. product looks like stew processed meat smells better. labrador finicky appreciates product better ."
    
  4. 执行词干提取过程,这将只保留单词的词根。例如,lookinglooked 将变为 look

    top_2_reviews <- tm_map(top_2_reviews,stemDocument)
    lapply(top_2_reviews[1], as.character)
    

    输出结果如下:

    ## [1] " bought sever vital can dog food product found good quality. product look like stew process meat smell better. labrador finicki appreci product better ."
    

    现在我们已经处理并清理了文本,我们可以创建一个文档矩阵,该矩阵仅存储两个评论中不同单词出现的频率。我们将演示如何计算评论中包含的每个单词。矩阵的每一行代表一个评论,列是不同的单词。大多数值都是零,因为并非每个评论都会包含所有单词。在这个例子中,稀疏度为 49%,这意味着矩阵中只有 51% 的值是非零的。

  5. 再次创建 as.matrix() 方法。矩阵包含两个文档(评论)和 37 个唯一单词。通过指定矩阵中的行和列索引或名称来检索文档中特定单词的计数。

  6. 现在,使用以下命令将结果存储在矩阵中:

    dtm_matrix <- as.matrix(dtm)
    
  7. 要找到矩阵的维度,即 2 个文档和 37 个单词,请使用以下命令:

    dim(dtm_matrix)
    

    输出结果如下:

    ## [1]  2 37
    
  8. 现在,打印矩阵的子集:

    dtm_matrix[1:2,1:7]
    

    输出结果如下:

    ##     Terms
    ## Docs "jumbo". actual appreci arriv better better. bought
    ##    1        0      0       1     0      1       1      1
    ##    2        1      1       0     1      0       0      0
    
  9. 最后,使用以下命令在文档 1 中计算单词 product

    dtm_matrix[1,"product"]
    

    输出结果如下:

    ## [1] 3
    

列表

虽然 vector 和 matrix 都是程序中用于各种计算的有用结构,但它们可能不足以存储现实世界的数据集,因为现实世界的数据集通常包含混合类型的数据,例如 CRM 应用程序中的客户表有两个列,一列是客户姓名,另一列是年龄。列表提供了一种结构,允许存储两种不同类型的数据。

在以下练习中,除了生成 16 个随机数外,我们还使用了 sample() 方法从英文字母表中生成 16 个字符。list 方法将整数和字符一起存储。

练习 6:使用列表方法存储整数和字符

在这个练习中,我们将使用 list 方法存储随机生成的数字和字符。随机数将使用 rbinom 函数生成,随机字符将从英文字母表 A-Z 中选择。

执行以下步骤来完成练习:

  1. 首先,生成参数大小为 100 且成功概率为 0.4 的二项分布的 16 个随机数:

    r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)
    
  2. 现在,从英语 LETTERS 中选择 16 个字母,不重复:

    #sample() will generate 16 random letters from the English alphabet without repetition
    r_characters <- sample(LETTERS, size = 16, replace = FALSE)
    
  3. r_numbersr_characters 放入一个单独的列表中。list() 函数将创建包含 r_numbersr_characters 的数据结构列表:

    list(r_numbers, r_characters)
    

    输出结果如下:

    ## [[1]]
    ##  [1] 48 53 38 31 44 43 36 47 43 38 43 41 45 40 44 50
    ## 
    ## [[2]]
    ##  [1] "V" "C" "N" "Z" "E" "L" "A" "Y" "U" "F" "H" "D" "O" "K" "T" "X"
    

    在以下步骤中,我们将看到一个包含整数和字符向量一起存储的列表。

  4. 现在,让我们将整数和字符向量存储和检索自一个列表:

    r_list <- list(r_numbers, r_characters)
    
  5. 接下来,使用以下命令检索字符向量中的值:

    r_list[[2]]
    

    输出如下:

    ##  [1] "V" "C" "N" "Z" "E" "L" "A" "Y" "U" "F" "H" "D" "O" "K" "T" "X"
    
  6. 最后,检索字符向量中的第一个值:

    (r_list[[2]])[1]
    

    输出如下:

    ## [1] "V" 
    

    虽然这解决了存储异构数据类型的要求,但它仍然没有对两个向量中的值之间的关系进行任何完整性检查。如果我们想将每个字母分配给一个整数。在先前的输出中,V代表48C代表53,以此类推。

    列表无法很好地处理这种一对一映射。考虑以下代码,如果我们生成 18 个随机字符,而不是16个字符,它仍然允许将它们存储在列表中。现在最后两个字符没有与整数相关联的映射。

  7. 现在,生成参数大小等于100,成功概率等于0.4的来自二项分布的 16 个随机数:

    r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)
    
  8. 从英语LETTERS中选择任意 18 个字母,且不重复:

    r_characters <- sample(LETTERS, 18, FALSE)
    
  9. r_numbersr_characters放入一个单独的列表中:

    list(r_numbers, r_characters)
    

    输出如下:

    ## [[1]]
    ##  [1] 48 53 38 31 44 43 36 47 43 38 43 41 45 40 44 50
    ## 
    ## [[2]]
    ##  [1] "V" "C" "N" "Z" "E" "L" "A" "Y" "U" "F" "H" "D" "O" "K" "T" "X" "P"  "Q"
    

活动二:创建两个矩阵的列表并访问其值

在这个活动中,你将创建两个矩阵,并使用矩阵的索引检索一些值。你还将执行乘法和减法等操作。

执行以下步骤以完成活动:

  1. 通过从二项分布(使用rbinom方法)随机生成数字来创建大小为10 x 44 x 5的两个矩阵。分别命名为mat_Amat_B

  2. 现在,将两个矩阵存储在一个列表中。

  3. 使用列表,访问mat_A的第 4 行第 2 列并将其存储在变量A中,以及访问mat_B的第 2 行第 1 列并将其存储在变量B中。

  4. AB矩阵相乘,并从mat_A的第 2 行第 1 列中减去。

    注意

    本活动的解决方案可在第 440 页找到。

DataFrame

由于向量、矩阵和列表的限制,对于数据科学从业者来说,需要一个适合现实世界数据集的数据结构,这是一个迫切需要的要求。DataFrame 是存储和检索表格数据的一种优雅方式。我们已经在练习 3读取 JSON 文件并将数据存储在 DataFrame 中中看到了 DataFrame 如何处理数据的行和列。DataFrame 将在整本书中广泛使用。

练习 7:使用 DataFrame 执行完整性检查

让我们回顾一下练习 6的第6 步使用列表方法存储整数和字符,其中我们讨论了在尝试将两个不等长向量存储在列表中时的完整性检查,并看看 DataFrame 是如何不同地处理它的。我们再次生成随机数字(r_numbers)和随机字符(r_characters)。

执行以下步骤以完成练习:

  1. 首先,生成 16 个来自参数大小等于100和成功概率等于0.4的二项分布的随机数:

    r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)
    
  2. 从英语LETTERS中选择任何 18 个字母,不重复:

    r_characters <- sample(LETTERS, 18, FALSE)
    
  3. r_numbersr_characters放入一个 DataFrame 中:

    data.frame(r_numbers, r_characters)
    

    输出如下:

    Error in data.frame(r_numbers, r_characters) : 
      arguments imply differing number of rows: 16, 18
    

    如您所见,前一个输出中的错误表明最后两个字母,即PQ,与使用二项分布生成的相应随机INTEGER没有映射。

访问 DataFrame 中的任何特定行和列与矩阵类似。我们将展示许多技巧和技术来最好地利用 DataFrame 中索引的强大功能,这还包括一些过滤选项。

DataFrame 中的每一行都是紧密耦合的列集合的结果。每一列清楚地定义了数据中的每一行与其他每一行之间的关系。如果没有相应的值在列中可用,它将被填充为 NA。例如,在 CRM 应用程序中,客户可能没有填写他们的婚姻状况,而其他一些客户填写了。因此,在应用程序设计期间指定哪些列是必需的,哪些是可选的变得至关重要。

数据表

随着 DataFrame 的日益普及,其局限性开始显现。特别是在处理大型数据集时,DataFrame 的表现不佳。在复杂分析中,我们经常创建许多中间 DataFrame 来存储结果。然而,R 建立在内存计算架构之上,并且高度依赖于 RAM。与磁盘空间不同,许多标准台式机和笔记本电脑的 RAM 限制在 4 或 8GB。DataFrame 在计算过程中没有高效地管理内存,这通常会导致“内存不足错误”,尤其是在处理大型数据集时。

为了处理这个问题,data.table继承了data.frame的功能,并在其基础上提供了快速且内存高效的版本,用于以下任务:

  • 文件读取器和写入器

  • 聚合

  • 更新

  • 等值、不等值、滚动、范围和区间连接

高效的内存管理使得开发快速,并减少了操作之间的延迟。以下练习展示了data.tabledata.frame相比在计算时间上的显著差异。首先,我们读取完整的fread()方法,这是data.table中快速读取方法之一。

练习 8:探索文件读取操作

在这个练习中,我们只展示文件读取操作。我们鼓励您测试其他功能(cran.r-project.org/web/packages/data.table/vignettes/datatable-intro.html)并比较数据表与 DataFrame 的能力。

执行以下步骤以完成练习:

  1. 首先,使用以下命令加载数据表包:

    library(data.table)
    
  2. 使用data.table包的fread()方法读取数据集:

    system.time(fread("Reviews_Full.csv"))
    

    输出如下:

    Read 14.1% of 568454 rows
    Read 31.7% of 568454 rows
    Read 54.5% of 568454 rows
    Read 72.1% of 568454 rows
    Read 79.2% of 568454 rows
    Read 568454 rows and 10 (of 10) columns from 0.280 GB file in 00:00:08
    ##    user  system elapsed 
    ##    3.62    0.15    3.78
    
  3. 现在,使用基础包中的read.csv()方法读取相同的 CSV 文件:

    system.time(read.csv("Reviews_Full.csv"))
    

    输出如下:

    ##    user  system elapsed 
    ##    4.84    0.05    4.91
    

注意,通过fread()方法读取它花费了3.78秒,而read.csv函数则花费了4.91秒。执行速度几乎快了30%。随着数据量的增加,这种差异变得更加显著。

在前面的输出中,user时间是当前 R 会话花费的时间,而system时间是操作系统完成该过程花费的时间。即使使用相同的数据集,执行system.time方法后也可能得到不同的值。这很大程度上取决于在运行方法时 CPU 有多忙。然而,我们应该将system.time方法的输出相对于我们进行的比较来读取,而不是相对于绝对值。

当数据集的大小太大时,我们有很多中间操作要完成最终输出。然而,请记住,data.table并不是一根魔杖,它不能让我们在 R 中处理任何大小的数据集。RAM 的大小仍然起着重要作用,data.table不能替代分布式和并行处理大数据系统。然而,即使是对于较小的数据集,data.table的使用也显示出比data.frames更好的性能。

数据处理和转换

到目前为止,我们已经看到了不同的读取和存储数据的方法。现在,让我们关注进行数据分析、提取见解或构建模型所需的数据处理和转换。原始数据几乎没有任何用途,因此将其处理成适合任何有用目的变得至关重要。本节重点介绍了 R 在数据分析中广泛使用的方法。

cbind

如其名所示,它通过列组合两个或多个向量、矩阵、DataFrame 或表。当我们需要将多个向量、矩阵或 DataFrame 组合成一个进行分析或可视化时,cbind非常有用。cbind的输出取决于输入数据。以下练习提供了几个cbind的例子,它结合了两个向量。

练习 9:探索 cbind 函数

在这个练习中,我们将实现cbind函数以组合两个 DataFrame 对象。

执行以下步骤以完成练习:

  1. 生成 16 个来自参数大小等于100和成功概率等于0.4的二项分布的随机数:

    r_numbers <- rbinom(n = 16, size = 100, prob = 0.4)
    
  2. 接下来,使用以下命令打印r_numbers值:

    r_numbers
    

    输出如下:

    ##  [1] 38 46 40 42 45 39 37 35 44 39 46 41 31 32 34 43
    
  3. 从英语LETTERS中选择任意 16 个字母,不重复:

    r_characters <- sample(LETTERS, 18, FALSE)
    
  4. 现在,使用以下命令打印r_characters值:

    r_characters
    

    输出如下:

    ##  [1] "C" "K" "Z" "I" "E" "A" "X" "O" "H" "Y" "T" "B" "N" "F" "U" "V" "S"
    ## [18] "P"
    
  5. 使用cbind组合r_numbersr_characters

    cbind(r_numbers, r_characters)
    

    输出如下:

    ## Warning in cbind(r_numbers, r_characters): number of rows of result is not a multiple of vector length (arg 1)
    ##       r_numbers r_characters
    ##  [1,] "38"      "C"         
    ##  [2,] "46"      "K"         
    ##  [3,] "40"      "Z"         
    ##  [4,] "42"      "I"         
    ##  [5,] "45"      "E"         
    ##  [6,] "39"      "A"         
    ##  [7,] "37"      "X"         
    ##  [8,] "35"      "O"         
    ##  [9,] "44"      "H"         
    "
    
  6. 打印使用cbind后获得的类(数据结构类型):

    class(cbind(r_numbers, r_characters))
    

    输出如下:

    ## [1] "matrix"
    

    观察到在练习的第 5 步中cbind输出的警告信息:

    number of rows of result is not a multiple of vector length (arg 1)
    r_numbers r_characters
    

这个错误意味着r_numbersr_characters的长度不相同(分别是 16 和 18)。请注意,与as.data.frame()不同,cbind()方法不会抛出错误。相反,它会自动执行所谓的r_numbers 3848从顶部回收以填充第 17 和第 18 个索引。

考虑我们编写以下命令而不是:

cbind(as.data.frame(r_numbers), as.data.frame(r_characters))

现在会抛出一个错误,正如我们在数据框部分之前所展示的:

Error in data.frame(..., check.names = FALSE) : 
  arguments imply differing number of rows: 16, 18

需要始终检查维度和数据类型,否则可能会导致不希望的结果。当我们给出两个向量时,通过cbind默认创建一个矩阵。

注意

由于我们没有设置任何种子值,因此每次执行代码时,样本和rbinom的输出都会不同。

rbind

rbind类似于cbind,但它通过行而不是列进行组合。为了使rbind工作,两个数据框中的列数应该相等。当我们在具有相同列的原始数据集中附加额外的观测值时,它很有用,其中原始数据集的所有列都是相同的,并且顺序相同。让我们在以下练习中探索rbind

练习 10:探索rbind函数

在这个练习中,我们将使用rbind函数组合两个数据框。

执行以下步骤来完成练习:

  1. 生成 16 个来自二项分布的随机数,参数大小等于100,成功概率等于 0.4:

    r_numbers <- rbinom(n = 18, size = 100, prob = 0.4)
    
  2. 接下来,打印r_numbers的值:

    r_numbers
    

    输出如下:

    ##  [1] 38 46 40 42 45 39 37 35 44 39 46 41 31 32 34 43
    
  3. 从英语LETTERS中选择任何 16 个字母,不重复:

    r_characters <- sample(LETTERS, 18, FALSE)
    
  4. 现在,使用以下命令打印r_characters

    r_characters
    

    输出如下:

    ##  [1] "C" "K" "Z" "I" "E" "A" "X" "O" "H" "Y" "T" "B" "N" "F" "U" "V" "S"
    ## [18] "P"
    
  5. 最后,使用rbind方法打印r_numbersr_characters的合并值:

    rbind(r_numbers, r_characters)
    

    输出如下:

    ##              [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11]
    ## r_numbers    "37" "44" "38" "38" "41" "35" "38" "40" "38" "45"  "37" 
    ## r_characters "Q"  "Y"  "O"  "L"  "A"  "G"  "V"  "S"  "B"  "U"   "D"  
    ##              [,12] [,13] [,14] [,15] [,16] [,17] [,18]
    ## r_numbers    "40"  "41"  "42"  "36"  "44"  "37"  "44" 
    ## r_characters "R"   "T"   "P"   "F"   "X"   "C"   "I"
    

从最后一步可以看出,rbind函数将r_numbersr_characters作为两行数据连接(绑定),这与cbind不同,在cbind中它是堆叠在两列中的。除了输出之外,cbind的所有其他规则也适用于rbind

合并函数

R 中的merge()函数在需要使用公共列(在数据库世界中我们称之为主键)连接多个数据框时特别有用。merge()函数对数据框和数据表有两种不同的实现方式,它们的行为基本上是相同的。

练习 11:探索合并函数

在这个练习中,我们将生成两个数据框,即df_onedf_two,使得r_numbers列在每个数据框中唯一标识每一行。

执行以下步骤来完成练习:

第一个数据框

  1. 使用set.seed()方法确保每次运行代码时生成相同的随机数:

    set.seed(100)
    
  2. 接下来,生成 1 到 30 之间的任意 16 个不重复的随机数:

    r_numbers <- sample(1:30,10, replace = FALSE)
    
  3. 从英文字母表中生成任意 16 个字符,允许重复:

    r_characters <- sample(LETTERS, 10, TRUE)
    
  4. r_numbersr_characters合并到一个名为df_one的 DataFrame 中:

    df_one <- cbind(as.data.frame(r_numbers), as.data.frame(r_characters))
    df_one
    

    输出如下:

    ##    r_numbers r_characters
    ## 1         10            Q
    ## 2          8            W
    ## 3         16            H
    ## 4          2            K
    ## 5         13            T
    ## 6         26            R
    ## 7         20            F
    ## 8          9            J
    ## 9         25            J
    ## 10         4            R
    

第二个 DataFrame

  1. 使用set.seed()方法来保留多次运行中相同的随机数:

    set.seed(200)
    
  2. 接下来,生成 1 到 30 之间的任意 16 个不重复的随机数:

    r_numbers <- sample(1:30,10, replace = FALSE)
    
  3. 现在,从英文字母表中生成任意 16 个字符,允许重复:

    r_characters <- sample(LETTERS, 10, TRUE)
    
  4. r_numbersr_characters合并到一个名为df_two的 DataFrame 中:

    df_two <- cbind(as.data.frame(r_numbers), as.data.frame(r_characters))
    df_two
    

    输出如下:

    ##    r_numbers r_characters
    ## 1         17            L
    ## 2         30            Q
    ## 3         29            D
    ## 4         19            Q
    ## 5         18            J
    ## 6         21            H
    ## 7         26            O
    ## 8          3            D
    ## 9         12            X
    ## 10         5            Q
    

一旦我们使用cbind()函数创建了df_onedf_twoDataFrame,我们就可以执行一些合并操作(将使用 JOIN 这个词,它与merge()的意思相同)。

现在,让我们看看不同类型的连接会产生不同的结果。

在数据库的世界中,JOIN 操作用于通过公共主键结合两个或两个以上的表。在数据库中,我们使用结构化查询语言(SQL)来执行 JOIN 操作。在 R 中,merge()函数帮助我们实现与 SQL 在数据库中提供的相同功能。此外,在这里我们不是使用表,而是使用 DataFrame,它也是一个具有行和列数据的表。

内连接

练习 11探索 merge 函数中,我们创建了两个 DataFrame:df_onedf_two。现在,我们将使用r_numbers列中两个 DataFrame 共有的26(行号7)来连接这两个 DataFrame,其中r_characters列中相应的字符在df_one中是R,在df_two中是O。在输出中,X对应于df_oneDataFrame,Y对应于df_twoDataFrame。

要使用r_numbers列将df_onedf_twoDataFrame 合并,使用以下命令:

merge(df_one, df_two, by = "r_numbers")
##   r_numbers r_characters.x r_characters.y
## 1        26              R              O

左连接

df_oner_numbers列中添加<NA>作为值,无论在df_two中找不到相应的值。例如,对于r_number = 2,在df_two中没有值,而对于r_number = 26df_onedf_twor_characters列的值分别是RO

要使用r_numbers列将df_onedf_twoDataFrame 合并,使用以下命令:

merge(df_one, df_two, by = "r_numbers", all.x = TRUE)
##    r_numbers r_characters.x r_characters.y
## 1          2              K           <NA>
## 2          4              R           <NA>
## 3          8              W           <NA>
## 4          9              J           <NA>
## 5         10              Q           <NA>
## 6         13              T           <NA>
## 7         16              H           <NA>
## 8         20              F           <NA>
## 9         25              J           <NA>
## 10        26              R              O

右连接

df_oner_character列在找不到匹配项的地方是<NA>。同样,r_numbers = 26是唯一的匹配项。

要使用r_numbers列将df_onedf_twoDataFrame 合并,使用以下命令:

merge(df_one, df_two, by = "r_numbers", all.y = TRUE)
##    r_numbers r_characters.x r_characters.y
## 1          3           <NA>              D
## 2          5           <NA>              Q
## 3         12           <NA>              X
## 4         17           <NA>              L
## 5         18           <NA>              J
## 6         19           <NA>              Q
## 7         21           <NA>              H
## 8         26              R              O
## 9         29           <NA>              D
## 10        30           <NA>              Q

全外连接

与左连接和右连接不同,r_numbers列从两个 DataFrame 中添加,并在r_characters列的相应 DataFrame 中添加<NA>。观察发现,只有r_number = 26这一行在两个 DataFrame 中都有值。

要使用r_numbers列将df_onedf_twoDataFrame 合并,使用以下命令:

merge(df_one, df_two, by = "r_numbers", all = TRUE)
##    r_numbers r_characters.x r_characters.y
## 1          2              K           <NA>
## 2          3           <NA>              D
## 3          4              R           <NA>
## 4          5           <NA>              Q
## 5          8              W           <NA>
## 6          9              J           <NA>
## 7         10              Q           <NA>
## 8         12           <NA>              X
## 9         13              T           <NA>
## 10        16              H           <NA>
## 11        17           <NA>              L
## 12        18           <NA>              J
## 13        19           <NA>              Q

reshape函数

数据通常在reshape函数中使用,经常用于在宽格式和长格式之间转换,以便进行各种操作,使数据对计算或分析有用。在许多可视化中,我们使用reshape()将宽格式转换为长格式,反之亦然。

我们将使用 Iris 数据集。这个数据集包含名为Sepal.LengthSepal.WidthPetal.LengthPetal.Width的变量,这些变量的测量值以厘米为单位,来自 3 种鸢尾花物种的每种 50 朵花,即setosaversicolorvirginica

练习 12:探索重塑函数

在这个练习中,我们将探索reshape函数。

执行以下步骤以完成练习:

  1. 首先,使用以下命令打印 iris 数据集的前五行:

    head(iris)
    

    上一条命令的输出如下:

    ##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
    ## 1          5.1         3.5          1.4         0.2  setosa
    ## 2          4.9         3.0          1.4         0.2  setosa
    ## 3          4.7         3.2          1.3         0.2  setosa
    ## 4          4.6         3.1          1.5         0.2  setosa
    ## 5          5.0         3.6          1.4         0.2  setosa
    ## 6          5.4         3.9          1.7         0.4  setosa
    
  2. 现在,根据以下条件创建一个名为Type的变量。当Sepal.Width > 2Sepal Width <= 3时,我们将分配TYPE 1TYPE 2。类型列仅用于演示目的,没有特定的逻辑:

    iris$Type <- ifelse((iris$Sepal.Width>2 & iris$Sepal.Width <=3),"TYPE 1","TYPE 2")
    
  3. TypeSepal.WidthSpecies列存储在df_iris数据框中:

    df_iris <- iris[,c("Type","Sepal.Width","Species")]
    
  4. 接下来,使用以下reshape命令将df_iris重塑为宽数据框:

    reshape(df_iris,idvar = "Species", timevar = "Type", direction = "wide")
    

    输出如下:

    ##        Species Sepal.Width.TYPE 2 Sepal.Width.TYPE 1
    ## 1       setosa                3.5                3.0
    ## 51  versicolor                3.2                2.3
    ## 101  virginica                3.3                2.7
    

    当运行reshape命令时,你会得到一个警告,如下所示:

    multiple rows match for Type=TYPE 2: first taken multiple rows match for Type=TYPE 1: first taken
    

这个警告意味着三种物种的Type 1Type 2有多个值,所以重塑选择了每种物种的第一个出现。在这种情况下,行号151101。我们现在将看到如何在aggregate函数中更好地处理这种转换。

聚合函数

聚合是一种有用的计算统计方法,如计数、平均值、标准差和四分位数,它还允许编写自定义函数。在下面的代码中,每个鸢尾花物种的公式(在 R 中,公式是数据结构的名字,而不是数学方程)计算了萼片和花瓣宽度和长度的数值测量值的平均值。聚合函数的第一个参数是一个公式,它接受物种和所有其他测量值,从所有观测值中计算平均值。

aggregate(formula =. ~ Species, data = iris, FUN = mean)

上一条命令的输出如下:

##      Species Sepal.Length Sepal.Width Petal.Length Petal.Width
## 1     setosa        5.006       3.428        1.462       0.246
## 2 versicolor        5.936       2.770        4.260       1.326
## 3  virginica        6.588       2.974        5.552       2.026

apply函数族

如果必须讨论 R 编程的几个强大功能,apply函数族将会被提及。它通常被用来避免使用 R 中可用的循环结构,如forwhile

首先,在 R 中运行for循环比较慢,其次,R 中apply函数的实现基于高效的编程语言,如 C/C++,这使得循环非常快。

apply函数族中有许多函数。根据所需的输入和输出结构,我们选择合适的函数:

  • apply()

  • lapply()

  • sapply()

  • vapply()

  • mapply()

  • rapply()

  • tapply()

我们将在本节讨论几个。

apply函数

apply() 函数接受一个数组,包括一个矩阵,作为输入,并返回通过将一个函数应用于数组的边缘或矩阵的边缘得到的值向量、数组或列表。

练习 13:实现 apply 函数

在这个练习中,我们将计算从英语字母表中随机生成的 100 x 100 矩阵中每一列的元音字母数量。MARGIN = 1 函数将扫描每一行,而 MARGIN = 2 将指定列。相同的函数将计算每一行的元音字母数量。

执行以下步骤以完成练习:

  1. 使用以下命令创建一个 100 x 100 的随机字母矩阵(ncol 是列数,nrow 是行数):

    r_characters <- matrix(sample(LETTERS, 10000, replace = TRUE), ncol = 100, nrow = 100)
    
  2. 现在,创建一个名为 c_vowel 的函数来计算给定数组中的元音字母数量:

    c_vowel <- function(x_char){
      return(sum(x_char %in% c("A","I","O","U")))
    }
    
  3. 接下来,使用 apply 函数遍历矩阵的每一列,并使用此处所示的 c_vowel 函数:

    apply(r_characters, MARGIN = 2, c_vowel)
    

    输出如下:

    ##   [1] 17 16 10 11 12 25 16 14 14 12 20 13 16 14 14 20 10 12 11 16 10 20 15
    ##  [24] 10 14 13 17 14 14 13 15 19 18 21 15 13 19 21 24 18 13 20 15 15 15 19
    ##  [47] 13  6 18 11 16 16 11 13 20 14 12 17 11 14 14 16 13 11 23 14 17 14 22
    ##  [70] 11 18 10 18 21 19 14 18 12 13 15 16 10 15 19 14 13 16 15 12 12 14 10
    ##  [93] 16 16 20 16 13 22 15 15
    

lapply 函数

lapply 函数看起来与 apply() 函数相似,不同之处在于它以 列表 作为输入并返回 列表 作为输出。在以下练习中重写我们之前的示例后,类函数的输出显示输出是一个列表。

练习 14:实现 lapply 函数

在这个练习中,我们将取一个向量列表并计算元音字母的数量。

执行以下步骤以完成练习:

  1. 创建一个包含两个随机字母向量的列表,每个向量的大小为 100:

    r_characters <- list(a=sample(LETTERS, 100, replace = TRUE),
                         b=sample(LETTERS, 100, replace = TRUE))
    
  2. 使用 lapply 函数遍历列表 ab,并使用 c_vowel 函数从列表中计算元音字母的数量:

    lapply(r_characters, c_vowel)
    

    输出如下:

    ## $a
    ## [1] 19
    ## $b
    ## [1] 10
    
  3. 检查输出的类(类型)。class() 函数提供数据结构的类型:

    out_list <- lapply(r_characters, c_vowel)
    class(out_list)
    

    输出如下:

    ## [1] "list"
    

sapply 函数

sapply 函数是 lapply 函数的一个包装器,其中输出是一个向量或矩阵而不是列表。在以下代码中,观察应用 sapply 差异后的输出类型。输出返回一个整数向量,我们可以通过 class() 函数来检查:

sapply(r_characters, c_vowel)
##  a  b 
## 19 10

要打印输出类的信息,请使用以下命令:

out_vector <- sapply(r_characters, c_vowel)
class(out_vector)

之前命令的输出如下:

## [1] "integer"

tapply 函数

将一个函数应用于稀疏数组的每个单元格,即应用于由某些因素级别的唯一组合给出的每个(非空)值组。当涉及到在数据子集级别工作时,tapply 函数非常有用。例如,在我们的 aggregate 函数中,如果我们想要获取 Iris 物种类型的标准差聚合,我们可以使用 tapply。以下代码显示了如何使用 tapply 函数:

首先,计算每个 Iris 物种花瓣长度的标准差:

tapply(iris$Sepal.Length, iris$Species,sd)

输出如下:

##     setosa versicolor  virginica 
##  0.3524897  0.5161711  0.6358796

接下来,计算每个 Iris 物种花瓣宽度的标准差:

tapply(iris$Sepal.Width, iris$Species,sd)

之前命令的输出如下:

##     setosa versicolor  virginica 
##  0.3790644  0.3137983  0.3224966

现在,让我们探索一些在构建复杂数据处理方法、机器学习模型或数据可视化时可能很有价值的流行且有用的 R 包。

有用的包

尽管 CRAN 仓库中有超过十三万个包,但其中一些包在某些主要功能方面具有独特的位置和用途。到目前为止,我们看到了许多数据操作示例,如连接、聚合、重塑和子集。接下来我们将讨论的 R 包将提供大量函数,提供广泛的数据处理和转换能力。

dplyr 包

dplyr包通过五种不同的方法帮助解决最常见的数据操作挑战,即mutate()select()filter()summarise()arrange()。让我们回顾一下来自 UCI 机器学习仓库的葡萄牙银行机构的直接营销活动(电话)数据集,以测试所有这些方法。

在以下练习中的%>%符号被称为链操作符。一个操作的结果被发送到下一个操作,而不需要显式创建一个新变量。这种链式操作是存储高效的,并且使代码的可读性变得容易。

练习 15:实现 dplyr 包

在这个练习中,我们感兴趣的是了解从事蓝领工作的人的平均银行余额,根据他们的婚姻状况。使用dplyr包中的函数来获取答案。

执行以下步骤以完成练习:

  1. 使用read.csv()函数将bank-full.csv文件导入到df_bank_detail对象中:

    df_bank_detail <- read.csv("bank-full.csv", sep = ';')
    
  2. 现在,使用以下命令加载dplyr库:

    library(dplyr)
    
  3. 选择(过滤)所有job列包含值blue-collar的观测值,然后按婚姻状况分组生成汇总统计量,即mean

    df_bank_detail %>%
      filter(job == "blue-collar") %>%
      group_by(marital) %>%
      summarise(
        cnt = n(),
        average = mean(balance, na.rm = TRUE)
      )
    

    输出如下:

    ## # A tibble: 3 x 3
    ##    marital   cnt   average
    ##     <fctr> <int>     <dbl>
    ## 1 divorced   750  820.8067
    ## 2  married  6968 1113.1659
    ## 3   single  2014 1056.1053
    
  4. 让我们找出具有中等教育水平和默认值为yes的客户的银行余额:

    df_bank_detail %>%
      mutate(sec_edu_and_default = ifelse((education == "secondary" & default == "yes"), "yes","no")) %>%
      select(age, job, marital,balance, sec_edu_and_default) %>%
      filter(sec_edu_and_default == "yes") %>%
      group_by(marital) %>%
      summarise(
        cnt = n(),
        average = mean(balance, na.rm = TRUE)
      )
    

    输出如下:

    ## # A tibble: 3 x 3
    ##    marital   cnt    average
    ##     <fctr> <int>      <dbl>
    ## 1 divorced    64   -8.90625
    ## 2  married   243  -74.46914
    ## 3   single   151 -217.43046
    

许多复杂分析都可以轻松完成。请注意,mutate()方法有助于创建具有特定计算或逻辑的自定义列。

tidyr 包

tidyr包有三个基本函数——gather()separate()spread()——用于清理混乱的数据。

gather()函数通过将多个列取出来并将它们聚合成键值对,将DataFrame 转换为长 DataFrame。

练习 16:实现 tidyr 包

在这个练习中,我们将探索tidyr包及其相关函数。

执行以下步骤以完成练习:

  1. 使用以下命令导入tidyr库:

    library(tidyr)
    
  2. 接下来,使用以下命令将seed设置为 100:

    set.seed(100)
    
  3. 创建一个r_name对象,并将 5 个人名存储在其中:

    r_name <- c("John", "Jenny", "Michael", "Dona", "Alex")
    
  4. 对于r_food_A对象,生成 1 到 30 之间的 16 个不重复的随机数:

    r_food_A <- sample(1:150,5, replace = FALSE)
    
  5. 类似地,对于r_food_B对象,生成 1 到 30 之间的 16 个不重复的随机数:

    r_food_B <- sample(1:150,5, replace = FALSE)
    
  6. 使用以下命令创建并打印 DataFrame 中的数据:

    df_untidy <- data.frame(r_name, r_food_A, r_food_B)
    df_untidy
    

    输出如下:

    ##    r_name r_food_A r_food_B
    ## 1    John       47       73
    ## 2   Jenny       39      122
    ## 3 Michael       82       55
    ## 4    Dona        9       81
    ## 5    Alex       69       25
    
  7. 使用tidyr包中的gather()方法:

    df_long <- df_untidy %>%
      gather(food, calories, r_food_A:r_food_B)
    df_long
    

    输出如下:

    ##     r_name     food calories
    ## 1     John r_food_A       47
    ## 2    Jenny r_food_A       39
    ## 3  Michael r_food_A       82
    ## 4     Dona r_food_A        9
    ## 5     Alex r_food_A       69
    ## 6     John r_food_B       73
    ## 7    Jenny r_food_B      122
    ## 8  Michael r_food_B       55
    ## 9     Dona r_food_B       81
    ## 10    Alex r_food_B       25
    
  8. spread()函数与gather()函数相反,即它将长格式转换为宽格式:

    df_long %>%
      spread(food,calories)
    ##    r_name r_food_A r_food_B
    ## 1    Alex       69       25
    ## 2    Dona        9       81
    ## 3   Jenny       39      122
    ## 4    John       47       73
    ## 5 Michael       82       55
    
  9. separate()函数在列是值组合的地方很有用,用于将其作为其他目的的关键列。如果它有一个共同的分隔符字符,我们可以分离出关键部分:

    key <- c("John.r_food_A", "Jenny.r_food_A", "Michael.r_food_A", "Dona.r_food_A", "Alex.r_food_A", "John.r_food_B", "Jenny.r_food_B", "Michael.r_food_B", "Dona.r_food_B", "Alex.r_food_B")
    calories <- c(74, 139, 52, 141, 102, 134, 27, 94, 146, 20)
    df_large_key <- data.frame(key,calories)  
    df_large_key
    

    输出如下:

    ##                 key calories
    ## 1     John.r_food_A       74
    ## 2    Jenny.r_food_A      139
    ## 3  Michael.r_food_A       52
    ## 4     Dona.r_food_A      141
    ## 5     Alex.r_food_A      102
    ## 6     John.r_food_B      134
    ## 7    Jenny.r_food_B       27
    ## 8  Michael.r_food_B       94
    ## 9     Dona.r_food_B      146
    ## 10    Alex.r_food_B       20
    df_large_key %>%
      separate(key, into = c("name","food"), sep = "\\.")
    ##       name     food calories
    ## 1     John r_food_A       74
    ## 2    Jenny r_food_A      139
    ## 3  Michael r_food_A       52
    ## 4     Dona r_food_A      141
    ## 5     Alex r_food_A      102
    ## 6     John r_food_B      134
    ## 7    Jenny r_food_B       27
    ## 8  Michael r_food_B       94
    ## 9     Dona r_food_B      146
    ## 10    Alex r_food_B       20
    

活动三:使用 dplyr 和 tidyr 从银行数据创建包含所有数值变量的五个汇总统计的 DataFrame

这个活动将使你习惯于从银行数据中选择所有数值字段,并对数值变量生成汇总统计。

执行以下步骤以完成活动:

  1. 使用select()从银行数据中提取所有数值变量。

  2. 使用summarise_all()方法,计算最小值、第一四分位数、第三四分位数、中位数、平均值、最大值和标准差。

    注意

    你可以在www.rdocumentation.org/packages/dplyr/versions/0.5.0/topics/summarise_all了解更多关于summarise_all函数的信息。

  3. 将结果存储在名为df_wide的宽格式 DataFrame 中。

  4. 现在,要将宽格式转换为深格式,请使用tidyr包中的gatherseparatespread函数。

  5. 最终输出应该为每个变量一行,以及最小值、第一四分位数、第三四分位数、中位数、平均值、最大值和标准差的每一列。

    一旦完成活动,你应该得到以下最终输出:

    ## # A tibble: 4 x 8
    ##        var   min   q25 median   q75    max       mean         sd
    ## *    <chr> <dbl> <dbl>  <dbl> <dbl>  <dbl>      <dbl>      <dbl>
    ## 1      age    18    33     39    48     95   40.93621   10.61876
    ## 2  balance -8019    72    448  1428 102127 1362.27206 3044.76583
    ## 3 duration     0   103    180   319   4918  258.16308  257.52781
    ## 4    pdays    -1    -1     -1    -1    871   40.19783  100.12875
    
    注意

    本活动的解决方案可以在第 440 页找到。

plyr 包

我们通过apply函数看到的情况可以通过plyr包在更大规模和稳健性上完成。plyr包提供了将数据集拆分为子集、对每个子集应用公共函数以及将结果合并为单个输出的能力。使用plyr而不是apply函数的优势包括以下特点:

  • 代码执行速度

  • 使用foreach循环进行处理的并行化。

  • 支持列表、DataFrame 和矩阵。

  • 更好的错误调试

plyr包中所有的函数名都是根据输入和输出明确定义的。例如,如果输入是 DataFrame,输出是列表,则函数名将是dlply

下图来自《数据分析的 Split-Apply-Combine 策略》论文,显示了所有不同的plyr函数:

图 1.7:plyr 包中的函数

图 1.7:plyr 包中的函数

_表示输出将被丢弃。

练习 17:探索 plyr 包

在这个练习中,我们将看到如何通过控制输入和输出的灵活性,split-apply-combine 方法使事情变得简单。

执行以下步骤以完成练习:

  1. 使用以下命令加载 plyr 包:

    library(plyr)
    
  2. 接下来,使用我们在早期示例 练习 13探索 apply 函数 中创建的 c_vowel 函数的略微修改版本:

    c_vowel <- function(x_char){
      return(sum(as.character(x_char[,"b"]) %in% c("A","I","O","U")))
    }
    
  3. seed 设置为 101

    set.seed(101)
    
  4. 将值存储在 r_characters 对象中:

    r_characters <- data.frame(a=rep(c("Split_1","Split_2","Split_3"),1000),
                         b= sample(LETTERS, 3000, replace = TRUE))
    
    注意

    输入 = DataFrame 输出 = 列表

  5. 使用 dlply() 函数并按行格式打印拆分:

    dlply(r_characters, c_vowel)
    

    输出如下:

    ## $Split_1
    ## [1] 153
    ## 
    ## $Split_2
    ## [1] 154
    ## 
    ## $Split_3
    ## [1] 147
    
    注意

    输入 = data.frame 输出 = 数组

  6. 我们可以简单地用 daply() 函数替换 dlply,并按列格式作为数组打印拆分:

    daply(r_characters, c_vowel)
    

    输出如下:

    ## Split_1 Split_2 Split_3 
    ##     153     154     147
    
    注意

    输入 = DataFrame 输出 = DataFrame

  7. 使用 ddply() 函数并打印拆分:

    ddply(r_characters, c_vowel)
    

    输出如下:

    ##         a  V1
    ## 1 Split_1 153
    ## 2 Split_2 154
    ## 3 Split_3 147
    

在步骤 5、6 和 7 中,观察我们如何创建了一个列表、数组以及作为 DataFrame 输出的数据。我们只需使用 plyr 的不同函数即可。这使得在许多可能的组合之间进行类型转换变得容易。

caret

caret 包特别适用于构建预测模型,它提供了一个无缝地跟随整个预测模型构建过程的框架。从数据拆分到训练和测试数据集以及变量重要性估计,我们将在回归和分类章节中广泛使用 caret 包。总之,caret 提供了以下工具:

  • 数据拆分

  • 预处理

  • 特征选择

  • 模型训练

  • 使用重采样进行模型调优

  • 变量重要性估计

我们将在 第四章回归第五章分类 中通过示例重新访问 caret 包。

数据可视化

我们所说的 ggplot2 是 R 中一个强大的包的一个基本部分。就像 dplyrplyr 一样,ggplot2 建立在 图形语法 的基础上,这是一种使我们能够简洁地描述图形组件的工具。

注意

良好的语法将使我们能够深入了解复杂图形的组成,并揭示看似不同图形之间意想不到的联系。

(Cox 1978) [Cox, D. R. (1978), "Some Remarks on the Role in Statistics of Graphical Methods," Applied Statistics, 27 (1), 4–9. [3,26].

散点图

散点图是一种使用笛卡尔坐标系显示数据集中通常两个变量的值的一种图表或数学图。如果点被着色,则可以显示一个额外的变量。

这是最常见的图表类型,在发现数据中的模式,尤其是在两个变量之间时非常有用。我们将再次使用我们的银行数据来进行一些 EDA。让我们使用葡萄牙银行直接营销活动数据集进行可视化:

df_bank_detail <- read.csv("bank-full.csv", sep = ';')

ggplot 以分层的方式堆叠图表的不同元素。在以下本节的示例中,在第一层,我们向 ggplot() 方法提供数据,然后使用诸如 xy 轴等美学细节进行映射,在示例中,分别是 agebalance 值。最后,为了能够识别与少数高银行余额相关的一些推理,我们根据工作类型添加了颜色。

执行以下命令以绘制年龄和余额的散点图:

ggplot(data = df_bank_detail) +
  geom_point(mapping = aes(x = age, y = balance, color = job))

![图 1.8:年龄和余额的散点图。

![img/C12624_01_08.jpg]

图 1.8:年龄和余额的散点图。

图 1.8 中,银行余额随年龄分布看起来非常正常,中年人显示出较高的银行余额,而年轻人和老年人位于光谱的较低端。

有趣的是,一些异常值似乎来自管理层和退休的专业人士。

在数据可视化中,看到图表并迅速得出结论总是很有诱惑力。数据可视化是为了更好地消费数据,而不是为了绘制因果推断。通常,分析师的解释总是要经过商业的审查。美观的图表往往会诱使你将其放入演示文稿中。所以,下次一个漂亮的图表进入你的演示文稿时,仔细分析你将要说的内容。

按婚姻状况分割的年龄和余额散点图

在本节中,我们将绘制三个散点图,这些散点图在年龄和余额按婚姻状况分割的单一图表中(每个单身、离婚和已婚个体一个)。

现在,你可以根据婚姻状况来分割分布。单身、已婚和离婚个体的模式似乎是一致的。我们使用了一种称为 facet_wrap() 的方法作为 ggplot 的第三层。它接受一个 marital 变量作为公式:

ggplot(data = df_bank_detail) +
  geom_point(mapping = aes(x = age, y = balance, color = job)) +
  facet_wrap(~ marital, nrow = 1)

![图 1.9:按婚姻状况分割的年龄和余额散点图

![img/C12624_01_09.jpg]

图 1.9:按婚姻状况分割的年龄和余额散点图

线形图

线形图或线形图是一种图表类型,它通过一系列称为 标记 的数据点显示信息,这些点由直线段连接。

ggplot 使用优雅的 geom() 方法,这有助于快速在两个视觉对象之间切换。在前面的示例中,我们看到了用于散点图的 geom_point()。在线形图中,观察结果按 x 轴上变量的顺序通过线连接。围绕线的阴影区域代表 95% 的置信区间,即有 95%的置信度认为实际的回归线位于阴影区域内。我们将在 第四章回归 中对此概念进行更多讨论。

在以下图表中,我们展示了单身、已婚和离婚个体的年龄和银行余额的线形图。是否有一些趋势尚不清楚,但可以看到三个类别之间的模式:

ggplot(data = df_bank_detail) +
  geom_smooth(mapping = aes(x = age, y = balance, linetype = marital))
## 'geom_smooth()' using method = 'gam'

![图 1.10:年龄和余额的线形图

![img/C12624_01_10.jpg]

图 1.10:年龄和余额的折线图

直方图

直方图是由矩形组成的可视化,其面积与变量的频率成正比,其宽度等于组距。

在直方图中,柱状的高度代表每个组中的观测值数量。在以下示例中,我们正在计算每种工作类型和婚姻状况的观测值数量。y是一个二进制变量,检查客户是否响应活动号召订阅了定期存款()。

看起来蓝领工人对活动号召的反应最少,而管理岗位的人对定期存款的订阅最多:

ggplot(data = df_bank_detail) +
  geom_bar(mapping = aes(x=job, fill = y)) +
  theme(axis.text.x = element_text(angle=90, vjust=.8, hjust=0.8))

![图 1.11:计数和工作的直方图图片

图 1.11:计数和工作的直方图

箱线图

箱线图是基于五个数值摘要(最小值、第一四分位数(Q1)、中位数、第三四分位数(Q3)和最大值)显示数据分布的一种标准化方式。箱线图可能是唯一一种与其他图表相比,在美观的表示中封装了更多信息图表。观察每个job类型对age变量的摘要。五个摘要统计量,即最小值、第一四分位数、中位数、均值、第三四分位数和最大值,通过箱线图简洁地描述。

第一四分位数和第三四分位数中的第 25 百分位数和第 75 百分位数分别由下四分位数和上四分位数表示。上四分位数,即从四分位数延伸到最大值的范围,在 1.5 * IQR(四分位距)内,这里的 IQR 是指四分位距,即两个四分位数之间的距离。下四分位数的情况类似。所有位于四分位数之外的数据点被称为异常值

tapply(df_bank_detail$age, df_bank_detail$job, summary)

输出如下:

## $admin.
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   20.00   32.00   38.00   39.29   46.00   75.00 
## 
## $'blue-collar'
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   20.00   33.00   39.00   40.04   47.00   75.00 
## 
## $entrepreneur
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   21.00   35.00   41.00   42.19   49.00   84.00 
## 
## $housemaid
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   22.00   38.00   47.00   46.42   55.00   83.00 
## 
## $management
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   21.00   33.00   38.00   40.45   48.00   81.00 
## 
## $retired
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   24.00   56.00   59.00   61.63   67.00   95.00 
## 
## $'self-employed'
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   22.00   33.00   39.00   40.48   48.00   76.00 
## 
0

在下面的箱线图中,我们正在查看每个工作类型的年龄摘要。在geom_boxplot中的varwidth = TRUE设置下,箱的大小显示了特定工作类型中的观测值数量。箱越宽,观测值数量就越多:

ggplot(data = df_bank_detail, mapping = aes(x=job, y = age, fill = job)) +
  geom_boxplot(varwidth = TRUE) +
  theme(axis.text.x = element_text(angle=90, vjust=.8, hjust=0.8))

![图 1.12:年龄和工作的箱线图图片

图 1.12:年龄和工作的箱线图

摘要

在本章中,我们介绍了 R 编程的基础数据类型、数据结构和重要的函数及包。我们描述了向量、矩阵、列表、DataFrame 和数据表作为存储数据的不同形式。在数据处理和转换空间中,我们探讨了cbindrbindmergereshapeaggregateapply函数族。

我们还讨论了 R 中最重要的包,如dplyrtidyrplyr。最后,使用ggplot2数据可视化包展示了各种可视化类型以及如何从中提取洞察。

在下一章中,我们将利用本章所学的一切来执行探索性数据分析(EDA)。在 EDA 中,您在这里学到的数据转换和可视化技巧将有助于从数据中得出推断。

第二章:数据的探索性分析

学习目标

到本章结束时,您将能够:

  • 使用行业标准框架定义问题陈述

  • 执行单变量和多变量分析

  • 解释多元分析

  • 执行假设检验

  • 使用 dplyr 和 reshape2 包进行数据整理

  • 使用 ggplot2 包可视化数据

在本章中,我们将向学习者介绍清洁、转换和可视化数据的技术,以便获得有用的见解。

简介

第一章R 高级分析,向您介绍了 R 语言及其数据科学生态系统。我们现在准备进入数据科学和机器学习的关键部分,那就是探索性数据分析EDA),理解数据的艺术。

在本章中,我们将使用与上一章相同的银行数据集来处理 EDA,但以更问题为中心的方式。我们将首先使用行业标准工件定义问题陈述,为问题设计解决方案,并学习 EDA 如何适应更大的问题框架。我们将使用 R 中的数据工程、数据整理和可视化技术,结合以业务为中心的方法,来处理葡萄牙银行机构的直接营销活动(电话)用例的 EDA。

在任何数据科学用例中,理解数据占据了大部分的时间和精力。大多数数据科学专业人士花费大约 80%的时间在理解数据上。鉴于这是您旅程中最关键的部分,对任何数据科学用例的整体过程有一个宏观的视角是很重要的。

典型的数据科学用例遵循核心业务分析问题或机器学习问题的路径。无论采取哪种路径,EDA 都是不可避免的。图 2.1展示了基本数据科学用例的生命周期。它从使用一个或多个标准框架定义问题陈述开始,然后深入数据收集并达到 EDA。在任何项目中,大部分的努力和时间都消耗在 EDA 上。一旦理解数据的过程完成,项目可能会根据用例的范围采取不同的路径。在大多数基于业务分析的使用案例中,下一步是将所有观察到的模式综合成有意义的见解。尽管这可能听起来很 trivial,但它是一个迭代且艰巨的任务。这一步随后演变为讲故事,其中浓缩的见解被定制成对业务利益相关者有意义的叙述。同样,在目标是为了开发预测模型的情况下,下一步将是实际开发机器学习模型并将其部署到生产系统/产品中。

图 2.1:数据科学用例的生命周期

图 2.1:数据科学用例的生命周期

让我们简要地看看第一步,定义问题陈述

定义问题陈述

如果您还记得我们在第一章中探讨的数据,高级分析中的 R,银行营销数据,我们有一个数据集,该数据集捕捉了银行进行的吸引客户的电话营销活动。

一家大型跨国银行正在设计一项营销活动,通过吸引客户存款来实现其增长目标。该活动在吸引客户方面效果不佳,营销团队希望了解如何改进活动以达到增长目标。

我们可以从业务利益相关者的角度重新构建问题,并尝试了解哪种解决方案最适合这里。

问题-设计工件

正如软件工程和其他工业项目有多个框架、模板和工件一样,数据科学和商业分析项目也可以使用行业标准工件有效地表示。一些流行的选择来自咨询巨头如麦肯锡、BCG,以及决策科学巨头如穆西伽。我们将使用一个基于Minto 金字塔原则的流行框架,称为情境-复杂性问题分析SCQ)。

让我们尝试以下结构来定义问题陈述:

  • 情境:定义当前情况。我们可以通过回答问题来简化这一点——发生了什么?

    一家大型跨国银行正在设计一项营销活动,通过吸引客户存款来实现其增长目标。该活动在吸引客户方面效果不佳,营销团队希望了解如何改进活动以达到增长目标。

在上一节中,我们看到了一个为银行数据用例构建的假设商业问题。尽管在现实中可能有所不同,但我们确实在尝试解决一个有效的用例。通过以前格式展示的格式表示问题陈述,我们有一个明确的关注和解决问题的领域。这解决了典型数据科学用例生命周期中的第一步。第二步是数据收集,我们在上一章中探讨了这一点。我们将参考由 UCI 机器学习存储库提供的相同数据集,网址为archive.ics.uci.edu/ml/datasets/Bank%20Marketing

备注

[Moro et al., 2014] S. Moro, P. Cortez, and P. Rita. A Data-Driven Approach to Predict the Success of Bank Telemarketing. Decision Support Systems, Elsevier, 62:22-31, June 2014.

这将我们带到了最后一步:EDA。在这个用例中,我们想要了解导致活动表现不佳的各种因素。在我们深入实际练习之前,让我们花点时间以更直观的方式理解 EDA 的概念。

理解 EDA 背后的科学

用通俗易懂的话来说,我们可以将 EDA 定义为理解数据的科学。更正式的定义是分析并探索数据集的过程,使用统计、视觉、分析或这些技术的组合来总结其特征、属性和潜在关系。

为了巩固我们的理解,让我们进一步分解定义。数据集是数值特征和分类特征的组合。为了研究数据,我们可能需要单独探索特征,而为了研究关系,我们可能需要一起探索特征。根据特征的数量和类型,我们可能会遇到不同类型的探索性数据分析(EDA)。

为了简化,我们可以将 EDA 的过程大致分为以下几类:

  • 单变量分析:研究单个特征

  • 双变量分析:研究两个特征之间的关系

  • 多元分析:研究两个以上特征之间的关系

现在,我们将本章的范围限制在单变量双变量分析上。在接下来的章节中,我们将介绍一些多元分析形式,如回归。

为了完成之前提到的每一项分析,我们可以使用可视化技术,如箱线图、散点图和条形图;统计技术,如假设检验;或者简单的分析技术,如平均值、频率计数等。

进一步细分,我们还有一个维度需要考虑,那就是特征的类型——数值分类。在提到的每种分析类型中——单变量双变量——根据特征类型,我们可能有不同的可视化技术来完成研究。因此,对于数值变量的单变量分析,我们可以使用直方图或箱线图,而对于分类变量,我们可能使用频率条形图。我们将以懒编程的方式深入了解 EDA 的整体练习,也就是说,我们将探索书中分析出现的上下文和细节。

在为练习设置基本背景后,让我们为特定的 EDA 练习做好准备。

探索性数据分析

我们将从可以从 UCI ML 存储库下载的数据集开始,该存储库的网址为archive.ics.uci.edu/ml/datasets/Bank%20Marketing

下载 ZIP 文件,将其解压到工作空间中的一个文件夹中,并使用名为bank-additional-full.csv的文件。要求学生启动一个新的 Jupyter 笔记本或他们选择的 IDE,并将数据加载到内存中。

练习 18:研究数据维度

让我们快速使用上一章中探索的简单命令加载数据,并查看数据集的一些基本特征。

我们正在探索数据的长度和宽度,即行数和列数、每列的名称、每列的数据类型以及每列存储内容的概览。

执行以下步骤以探索银行数据集:

  1. 首先,在 RStudio 中导入所有必需的库:

    library(dplyr)
    library(ggplot2)
    library(repr)
    library(cowplot)
    
  2. 现在,使用option方法将图表的widthheight分别设置为124

    options(repr.plot.width=12, repr.plot.height=4)
    

    确保您下载并将bank-additional-full.csv文件放置在适当的文件夹中。您可以从bit.ly/2DR4P9I访问该文件。

  3. 创建一个 DataFrame 对象并使用以下命令读取 CSV 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  4. 现在,使用以下命令显示数据集中的数据:

    str(df)
    

    输出如下:

图 2.2:来自 bank-additional-full CSV 文件的银行数据

图片

图 2.2:来自 bank-additional-full CSV 文件的银行数据

在前面的例子中,我们使用了 R 中可用的传统read.csv函数将文件读入内存。由于文件是分号分隔的,我们向sep=";"函数添加了一个参数。str函数打印出我们所需的数据集的高级信息。如果您仔细观察输出片段,您可以看到第一行表示数据的形状,即行数/观测数和列数/变量数。

输出片段中的下 21 行展示了数据集中每个变量的预览。它显示了变量的名称、其数据类型和数据集中的一些值。我们为每一列有一个条目。str函数实际上为我们提供了整个数据集的宏观视图。

如您从数据集中所见,我们有 20 个独立变量,例如agejobeducation,以及一个结果/因变量y。在这里,结果变量定义了向客户发起的营销电话是否导致了成功的存款注册,用yesno表示。为了理解整个数据集,我们现在需要研究数据集中的每个变量。让我们首先进行单变量分析。

单变量分析

对于数据集中的数值特征(如agedurationnr.employed)等,我们查看诸如最小值、最大值、平均值、标准差和百分位数分布等汇总统计量。这些度量共同帮助我们了解数据的分布。同样,对于分类特征(如jobmaritaleducation),我们需要研究特征中的不同值及其频率。为了完成这项任务,我们可以实施一些分析、可视化和统计技术。让我们看看探索数值特征的解析和可视化技术。

探索数值/连续特征

如果你探索了前面的输出片段,你可能已经注意到数据集中有数值型和分类型特征的混合。让我们首先查看数据集中的第一个特征,它是一个名为age的数值型特征。正如其名所示,它表示目标客户的年龄。让我们看一下该特征的摘要统计信息,并使用简单的箱线图进行可视化。

练习 19:使用箱线图可视化数据

在这个练习中,我们将探索使用箱线图进行单变量分析,解释如何解释箱线图,并逐步展示代码。

执行以下步骤以使用箱线图可视化数据:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用bank-additional-full.csv文件,使用以下命令:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 使用以下命令打印age数据,例如meanmax

    print(summary(df$age))
    

    输出如下:

    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
    17.00   32.00   38.00   40.02   47.00   98.00
    
  4. 接下来,按照以下方式打印年龄的标准差:

    print(paste("Std.Dev:",round(sd(df$age),2)))
    

    输出如下:

    [1] "Std.Dev: 10.42"
    
  5. 现在,使用以下参数绘制年龄的箱线图:

    ggplot(data=df,aes(y=age)) + geom_boxplot(outlier.colour="black")
    

    输出如下:

![图 2.3:年龄的箱线图。![图 2.3:年龄的箱线图。###### 图 2.3:年龄的箱线图。我们首先加载ggplot2库,它提供了用于可视化数据的便捷函数。R 提供了一个简单的函数summary,它打印摘要统计信息,如最小值、最大值、中位数、平均值、75 百分位数和 25 百分位数值。下一行使用sd函数计算标准差,最后,最后一行使用ggplot库绘制数据的箱线图。如果你探索了摘要统计信息的输出,我们可以看到年龄的最小值为 17,最大值为 98,平均值为 42。如果你仔细观察 75 百分位数(第三四分位数)和 100 百分位数(最大值)之间的差距,我们可以看到巨大的跳跃。这表明年龄变量中存在异常值。异常值的存在将错误地改变你的分析结论。在某些情况下,当只有一个值为 75 百分位数的1000x的数据点时,你的平均值会向右移动。在仅使用平均值作为对变量进行估计的大致数值的情况下,对特征的整体理解可能会产生误导。另一方面,箱线图帮助我们以简单明了的方式直观地消费这些信息。箱线图将数据分为三个四分位数。下四分位数,即箱体下方的线,代表最小值和 25 百分位数。中间四分位数代表 25 至 50 至 75 百分位数。上四分位数代表 75 至 100 百分位数。100 百分位数以上的点是由内部函数确定的异常值。正如我们所见,从摘要统计中得出的观察结果与箱线图一致。我们确实看到了异常值,标记为上四分位数以上的点。在下一个练习中,我们将使用直方图对年龄变量进行 EDA。让我们看看从直方图图中我们能得到哪些见解。### 练习 20:使用直方图可视化数据在这个练习中,我们将讨论如何解释直方图和异常值。让我们从上一个练习继续。为了获得数据的更详细视图并更深入地理解 age 变量的组织方式,我们可以使用直方图。直方图是一种特殊的条形图,其中数据被分组并按顺序排列到称为 bins 的相等间隔中,并绘制相应分箱中的数据点的频率。直方图帮助我们更有效地理解数据的分布。练习通过绘制直方图帮助我们更有效地可视化数据。执行以下步骤:1. 首先,使用以下命令导入 ggplot2 包: py library(ggplot2) 1. 创建一个 DataFrame 对象 df,并使用以下命令使用 bank-additional-full.csv 文件: py df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';') 1. 现在,使用以下命令使用提供的参数绘制年龄的直方图: py ggplot(data=df,aes(x=age)) + geom_histogram(bins=10,fill="blue", color="black", alpha =0.5) + ggtitle("Histogram for Age") + theme_bw() 输出如下:图 2.4:年龄直方图

图 2.4:年龄直方图

ggplot 函数定义了可视化的基础层,随后通过带有参数的 geom_histogram 函数,这些参数定义了直方图相关方面,例如分箱数、填充颜色、alpha(透明度)等。默认情况下,分箱数也会被计算,但可以通过传递一个值给 bin 参数来覆盖,例如 bin=10。下一个函数 ggtitle 用于给图表添加标题,而 theme_bw 函数用于将主题改为黑白而非默认。theme 函数是可选的,这里添加它只是为了使图表更美观。

如您所清晰看到的,直方图为我们提供了对特征数据分布的更细粒度的视图。我们可以理解,65 岁之后记录数急剧减少,只有少数记录的值超过 75。在某些情况下,选择直方图的分箱数变得很重要,因为更多的分箱会使分布变得混乱,而较少的分箱会使分布信息不足。在我们想要看到分布的更细粒度视图的情况下,而不是增加直方图的分箱数,我们可以选择使用密度图来可视化,该图在连续区间上可视化,同时使用核平滑来平滑噪声。

我们也可以使用密度图而不是直方图来可视化年龄变量。下一个练习将详细介绍如何实现。

练习 21:使用密度图可视化数据

在这个练习中,我们将演示相同特征 age 的密度图。

执行以下步骤:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 现在,使用以下命令绘制年龄的密度图:

    ggplot(data=df,aes(x=age)) + geom_density(fill="red",alpha =0.5) + 
                                 ggtitle("Density Plot for Age") + 
                                 theme_bw()
    

    输出如下:

![图 2.5:年龄的密度图。

![img/C12624_02_05.jpg]

图 2.5:年龄的密度图。

与上一个练习类似,我们使用ggplot函数的相同基础进行可视化,并使用不同的geom_density函数进行密度图绘制。其他用于可视化的附加函数保持不变。

密度图比直方图提供更详细的信息。虽然通过增加直方图的分组数也可以达到这种详细程度,但通常需要通过试错法来确定最佳的分组数。在这种情况下,选择密度图会是一个更简单的选项。

现在我们已经了解了数值变量的单变量分析的概念,让我们加快对其他变量的数据探索。我们总共有 10 个分类特征和 10 个数值列。让我们尝试使用直方图一起查看四个数值变量。

就像我们绘制年龄的直方图一样,我们可以通过定义一个自定义函数同时为多个变量绘制直方图。下一个练习将展示如何做到这一点。

练习 22:使用直方图可视化多个变量

在这个练习中,我们将把四个直方图(每个变量一个)组合成一个单独的图表。我们有campaign,表示在活动中进行的联系次数,pdays表示自上次前一个活动联系客户以来的天数;值为 999 表示客户之前从未被联系过。previous表示为此客户之前进行的联系次数,最后,emp.var.rate表示就业方差率。

为了完成这个练习,我们执行以下步骤:

  1. 首先,使用以下命令导入cowplot包:

    library(cowplot)
    

    确保已安装cowplot包。

  2. 接下来,定义一个函数来绘制所有数值列的直方图:

    plot_grid_numeric <- function(df,list_of_variables,ncols=2){
        plt_matrix<-list()
        i<-1
        for(column in list_of_variables){
            plt_matrix[[i]]<-ggplot(data=df,aes_string(x=column)) + 
                geom_histogram(binwidth=2,fill="blue", color="black", 
                               alpha =0.5)  +
                ggtitle(paste("Histogram for variable: ",column)) + theme_bw()
                i<-i+1
                }
        plot_grid(plotlist=plt_matrix,ncol=2)
    }
    
  3. 现在,使用summary函数打印campaignpdayspreviousemp.var.rate列的均值、最大值和其他参数:

    summary(df[,c("campaign","pdays","previous","emp.var.rate")])
    

    输出如下:

       campaign          pdays          previous      emp.var.rate     
    Min.   : 1.000   Min.   :  0.0   Min.   :0.000   Min.   :-3.40000  
    1st Qu.: 1.000   1st Qu.:999.0   1st Qu.:0.000   1st Qu.:-1.80000  
    Median : 2.000   Median :999.0   Median :0.000   Median : 1.10000  
    Mean   : 2.568   Mean   :962.5   Mean   :0.173   Mean   : 0.08189  
    3rd Qu.: 3.000   3rd Qu.:999.0   3rd Qu.:0.000   3rd Qu.: 1.40000  
    Max.   :56.000   Max.   :999.0   Max.   :7.000   Max.   : 1.40000
    
  4. 调用我们之前定义的函数来绘制直方图:

    plot_grid_numeric(df,c("campaign","pdays","previous","emp.var.rate"),2)
    

    输出如下:

![图 2.6:使用直方图可视化多个变量

![img/C12624_02_06.jpg]

图 2.6:使用直方图可视化多个变量

在这个练习中,我们自动化了将多个同类型图堆叠成一个综合图的过程。我们首先加载所需的cowplot库。这个库为使用ggplot库渲染的图提供了创建图网格的便捷函数。如果你还没有加载这个库,请使用install.packages('cowplot')命令安装包。然后我们定义一个名为plot_grid_numeric的函数,它接受数据集、要绘制的特征列表以及网格中要使用的列数作为参数。如果你观察函数的内部实现,你会看到我们只是使用for循环遍历提供的变量列表,并将单个图收集到一个名为plt_matrix的列表中。稍后,我们使用cowplot库提供的plot_grid函数将图排列成两列的网格。

同一个函数可以用来显示任何数量的直方图网格;使用基于你屏幕大小的数字。当前数字已被限制为 4 以获得最佳效果。我们还使用summary函数与直方图图一起显示相同一组数值变量的总体统计信息。

注意

在前面的函数中没有使用异常处理代码。我们目前忽略了实现复杂的代码,以便专注于感兴趣的主题。如果在非数值变量上使用该函数,错误信息可能不是最有效的解决方案。

如前一个图所示,我们现在有四个变量一起进行分析。将摘要统计信息与直方图图一起研究有助于我们更好地揭示变量。Campaign有 75%的值小于或等于 3。我们可以看到有一个异常值在 56,但绝大多数的记录值小于 5。pdays似乎不是我们分析的有用变量,因为几乎所有记录都有默认值 999。1000 中的高柱状图清楚地表明,几乎没有记录的值不是 999。

对于previous变量,我们看到它与pdays正好相反;大多数记录的值为 0。最后,emp.var.rate显示了一个有趣的结果。尽管值范围从-42,但超过一半的记录具有正值。

因此,通过分析这四个变量,我们可以大致得出结论,之前进行的营销活动并没有经常通过电话与客户联系,或者这也可能意味着在之前的营销活动中,几乎没有针对当前营销活动的目标客户进行联系。而且,较早联系的客户最多被联系了七次。客户上次被联系的天数自然与之前营销活动的结果一致,因为几乎没有人在之前被联系过。然而,对于当前营销活动,客户平均被联系了 2.5 次,75% 的客户被联系了 3 次或更少,一些客户被联系次数高达 56 次。就业方差率是衡量由于宏观经济状况而雇佣或解雇人数的指标。我们了解到,在营销活动的大部分时间里,经济状况相对稳定。

与上一节中创建的用于堆叠直方图的函数类似,在此活动中,我们将创建另一个用于堆叠密度图和另一个用于箱线图的函数。

活动 4:绘制多个密度图和箱线图

在此活动中,我们将创建一个用于堆叠密度图的函数,另一个用于箱线图。使用新创建的函数来可视化与上一节相同的变量集,并研究分析数值变量的最有效方法。

到此活动结束时,你将学会如何同时绘制多个变量的密度图。这样做可以一次比较不同的变量,并从中得出数据洞察。

完成此活动的步骤如下:

  1. 首先,在 RStudio 中加载必要的库和包。

  2. bank-additional-full.csv 数据集读入名为 df 的 DataFrame。

  3. 定义用于密度图的 plot_grid_numeric 函数:

    plot_grid_numeric <- function(df,list_of_variables,ncols=2){
      plt_matrix<-list()
      i<-1
      }
      plot_grid(plotlist=plt_matrix,ncol=2)
    }
    
  4. 绘制 campaignpdayspreviousemp.var.rate 变量的密度图:图 2.7:campaign、pdays、previous 和 emp.var.rate 变量的密度图

    图 2.7:campaignpdayspreviousemp.var.rate 变量的密度图

    观察到我们使用直方图获得的解释在密度图中同样明显。因此,这可以作为查看相同趋势的另一种替代图表。

  5. 重复箱线图的步骤:

    plot_grid_numeric <- function(df,list_of_variables,ncols=2){
      plt_matrix<-list()
      i<-1
    }
    plot_grid_numeric(df,c("campaign","pdays","previous","emp.var.rate"),2)
    

    图形如下:

图 2.8:campaign、pdays、previous 和 emp.var.rate 变量的箱线图

图 2.8:campaignpdayspreviousemp.var.rate 变量的箱线图

在箱线图中需要注意的一个额外点是,它显示了campaign变量中存在明显的异常值,这在其他两个图表中并不非常明显。对于previouspdays变量也可以做出类似的观察。学生应该尝试在移除异常值后绘制箱线图,看看它们看起来有何不同。

注意

你可以在第 442 页找到这个活动的解决方案。

练习 23:绘制 nr.employed、euribor3m、cons.conf.idx 和 duration 变量的直方图

在这个练习中,我们将转向下一组也是最后一组四个数值变量。我们有nr.employed,表示在银行工作的员工人数,euribor3m表示 3 个月欧元间银行利率的平均利率。此外,我们还有cons.conf.index,这是消费者信心指标,通过消费者通过储蓄和消费活动表达的对国家乐观程度的程度来衡量。最后,是duration,表示最后一次联系持续时间。根据 UCI 提供的元数据,这个变量与结果高度相关,可能导致数据泄露。因此,我们将从未来的分析中删除这个变量。

执行以下步骤以研究下一组数值变量:

  1. 首先,使用以下命令导入cowplot包:

    library(cowplot)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 使用summary方法打印详细信息:

    summary(df[,c("nr.employed","euribor3m","cons.conf.idx","duration")])
    

    输出如下:

     nr.employed     euribor3m     cons.conf.idx      duration     
    Min.   :4964   Min.   :0.634   Min.   :-50.8   Min.   :   0.0  
    1st Qu.:5099   1st Qu.:1.344   1st Qu.:-42.7   1st Qu.: 102.0  
    Median :5191   Median :4.857   Median :-41.8   Median : 180.0  
    Mean   :5167   Mean   :3.621   Mean   :-40.5   Mean   : 258.3  
    3rd Qu.:5228   3rd Qu.:4.961   3rd Qu.:-36.4   3rd Qu.: 319.0  
    Max.   :5228   Max.   :5.045   Max.   :-26.9   Max.   :4918.0
    
  4. 绘制定义变量的直方图,如下命令所示:

    plot_grid_numeric(df,c("nr.employed","euribor3m","cons.conf.idx","duration"),2)
    

    输出如下:

![图 2.9:各种变量的计数和持续时间的直方图img/C12624_02_09.jpg

图 2.9:各种变量的计数和持续时间的直方图

就像练习 5使用直方图可视化多个变量,我们首先使用summary函数对所需的一组变量进行汇总统计,然后通过调用我们之前定义的相同函数,一起绘制所有所需变量的组合直方图。

如我们所见,雇佣的员工数量大部分保持在5228,但在时间期间也降至不同的值。这个数字是按季度测量的,因此频率并不非常动态,这就是为什么我们可以看到围绕仅几个桶的值。欧元间银行利率大部分在2.55之间。只有 1 或 2 条记录的值超过 5,我们可以看到这个变量的最大测量值为5.045。消费者信心指数大部分为负值,这意味着消费者在此时大部分对经济状况持负面看法。我们在直方图的桶中看到两个峰值,这表明了当时最常见的信心指数,并模糊地暗示了在活动期间指数的有限变化。现在,我们暂时忽略通话时长(以秒为单位)的分析。

总结一下,我们了解到在活动中,银行的员工数量在约 250 人的范围内有所增加和减少,这大约是总员工数的 5%。员工数量在49645228之间,变化不大。消费者信心指数在时间期间大部分保持负值,变化不大,欧元间银行利率的平均值为 3.6,大部分记录在 2.5 到 5 之间。

现在,让我们继续研究使用单变量分析来研究分类型变量。

探索分类型特征

分类型特征在本质上与数值或连续特征不同,因此之前使用的传统方法在这里不适用。我们可以分析分类型变量中不同类的数量以及与每个类相关的频率。这可以通过简单分析技术或视觉技术实现。让我们通过结合这两种方法来探索一系列分类型特征。

练习 24:探索分类型特征

在这个练习中,我们将从一个简单的变量开始,即marital,它表示客户的婚姻状况。让我们使用dplyr库来进行分组数据聚合。

执行以下步骤以完成练习:

  1. 首先,使用以下命令在系统中导入dplyr库:

    library(dplyr)
    
  2. 接下来,我们将创建一个名为marital_distribution的对象,并根据以下条件存储值:

    marital_distribution <- df %>% group_by(marital) %>% 
                                   summarize(Count = n()) %>% 
                                   mutate(Perc.Count = round(Count/sum(Count)*100))
    
  3. 现在,打印存储在marital_distribution对象中的值:

    print(marital_distribution)
    

    输出如下:

    # A tibble: 4 x 3
      marital  Count Perc.Count
      <fct>    <int>      <dbl>
    1 divorced  4612         11
    2 married  24928         61
    3 single   11568         28
    4 unknown     80          0
    

为了计数分类列中的不同类别的数量,以及获取每个个别类别的记录计数,我们使用 dplyr 库下可用的 group_by 函数。%>%,也称为 marital,然后将输出传递给 summarize 函数,该函数使用我们提供的聚合函数将 DataFrame 聚合到分组级别;在这种情况下,n() 是简单的 count 等价。最后,我们使用 mutate 函数计算每个个别组成员的计数百分比。

我们可以看到,大多数竞选电话是打给已婚客户的,约占 61%,其次是 28% 的单身客户电话,等等。

练习 25:使用柱状图探索分类特征

在这个练习中,我们将绘制一个柱状图,以可视化每个类别的频率计数。我们也可以使用柱状图来表示每个这些个别类别的频率分布图。

执行以下步骤以完成练习:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令使用 bank-additional-full.csv 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 现在,使用以下命令按计数绘制婚姻状况的柱状图:

    ggplot(data = marital_distribution,aes(x=marital,y=Perc.Count)) + 
                  geom_bar(stat="identity",fill="blue",alpha=0.6) + 
                  geom_text(aes(label=marital_distribution$Perc.Count, vjust = -0.3))
    

    输出如下:

图 2.10:按计数显示的婚姻状况柱状图

图 2.10:按计数显示的婚姻状况柱状图

我们使用与之前片段中相同的工程化数据集,该数据集计算每个类别的频率及其相对百分比。要绘制柱状图,我们使用 ggplot 的相同基础函数,其中我们定义了 xy 变量的美学,并使用 geom_bar 函数附加柱状图。geom_text 函数允许我们在图表中的每个条形上添加标签。

我们现在可以看到与之前练习中相同的数字,这里通过柱状图可视化。在变量中有大量类别的情况下,逐个查看每个类别来研究它们可能不是最有效的方法。一个简单的图表可以轻松帮助我们以易于消费的方式理解分类变量的频率分布。

练习 26:使用饼图探索分类特征

在这个练习中,我们将定义饼图及其内部的各种组件。

与柱状图类似,我们还有一个饼图,它使理解类别的百分比分布更容易。执行以下步骤以使用饼图可视化相同的变量,即婚姻状况:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令使用 bank-additional-full.csv 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 接下来,使用以下命令定义标签位置:

    plot_breaks = 100 - (cumsum(marital_distribution$Perc.Count) - 
                       marital_distribution$Perc.Count/2)
    
  4. 现在,为图表定义标签:

    plot_labels = paste0(marital_distribution$marital,"-",marital_distribution$Perc.Count,"%")
    
  5. 设置图表大小以获得更好的视觉效果:

    options(repr.plot.width=12, repr.plot.height=8)
    
  6. 使用以下命令创建饼图:

    ggplot(data = marital_distribution,aes(x=1,y=Perc.Count, fill=marital)) + 
                  geom_bar(stat="identity") + #Creates the base bar visual
                  coord_polar(theta ="y")  + #Creates the pie chart
                  scale_y_continuous(breaks=plot_breaks, labels = plot_labels,position = "left") + 
                  theme(axis.text.x = element_text(angle = 30, hjust =1)) + #rotates the labels
                  theme(text = element_text(size=15)) + #increases the font size for the legend
                  ggtitle("Percentage Distribution of Marital Status") #Adds the plot title
    

![图 2.11:婚姻状况的百分比分布饼图]

![img/C12624_02_11.jpg]

图 2.11:婚姻状况的百分比分布饼图

我们首先定义一些额外的变量,这将帮助我们更容易地获取图表。为了标记饼图,我们需要断点(break points)和实际标签。理想情况下,断点应位于饼图的中间部分。因此,我们计算百分比分布的累积总和,并从每个类别中减去一半,以找到该部分的中间点。然后,我们从 100 中减去整个数值,以顺时针方向排列标签。

下一步定义每个饼图的标签;我们使用paste函数将标签名称和实际百分比值连接起来。ggplot中的饼图功能是通过在柱状图之上构建元素来实现的。我们首先使用ggplotgeom_bar的基础来渲染堆叠柱状图的基础,并使用coord_polar函数将其转换为所需的饼图。scale_y_continuous函数有助于将标签放置在饼图分布上。下一行添加了文本定位的旋转角度。theme函数的element_text部分中的size参数定义了图中文本的字体大小。其余部分与我们在早期图表中学习的一样。

我们可以看到,饼图为我们提供了一个直观的方式来探索每个变量中类别的百分比分布。关于选择饼图而不是柱状图的建议,应基于变量中不同类别的数量。尽管饼图在视觉上更吸引人,但类别众多时,饼图会显得过于拥挤。

注意

当分类变量中不同类别的数量较高时,最好避免使用饼图。没有明确的规则,但任何使饼图视觉上杂乱无章的因素都不适合研究。

练习 27:自动化绘图分类变量

在这个练习中,我们将自动化分类变量的绘图。

就像数值变量一样,我们也有 10 个分类变量,不包括目标变量。类似于自动化数值特征的探索,现在让我们为分类变量创建一个函数。为了使事情简单,我们将主要使用带有百分比分布的箱线图而不是饼图。我们将从四个分类特征开始,然后转到下一个剩余的集合。

执行以下步骤以完成练习:

  1. 首先,使用以下命令导入cowplot包:

    library(cowplot)
    
  2. 定义一个函数来绘制所有数值列的直方图:

    plot_grid_categorical <- function(df,list_of_variables,ncols=2){
        plt_matrix <- list()
        i<-1
        #Iterate for each variable
        for(column in list_of_variables){
            #Creating a temporary DataFrame with the aggregation
            var.dist <- df %>% group_by_(column) %>% 
                               summarize(Count = n()) %>% 
                               mutate(Perc.Count = round(Count/sum(Count)*100,1))
            options(repr.plot.width=12, repr.plot.height=10)
            plt_matrix[[i]]<-ggplot(data = var.dist,aes_string(x=column,y="Perc.Count")) +
                geom_bar(stat="identity",fill="blue",alpha=0.6) + #Defines the bar plot
                geom_text(label=var.dist$Perc.Count,vjust=-0.3)+  #Adds the labels
                theme(axis.text.x = element_text(angle = 90, vjust = 1)) + #rotates the labels
                ggtitle(paste("Percentage Distribution of variable: ",column))  #Creates the title +
                i<-i+1
        }
            plot_grid(plotlist=plt_matrix,ncol=ncols) #plots the grid
    }
    
  3. 接下来,使用以下命令调用summary统计量:

    summary(df[,c("job","education","default","contact")])
    

    输出如下:

              job                      education        default           contact     
     admin.     :10422   university.degree  :12168   no     :32588   cellular :26144  
     blue-collar: 9254   high.school        : 9515   unknown: 8597   telephone:15044  
     technician : 6743   basic.9y           : 6045   yes    :    3
     services   : 3969   professional.course: 5243
     management : 2924   basic.4y           : 4176
     retired    : 1720   basic.6y           : 2292
     (Other)    : 6156   (Other)            : 1749
    
  4. 调用我们之前定义的函数来绘制直方图:

    plot_grid_categorical(df,c("job","education","default","contact"),2)
    

    输出如下:

![图 2.12:分类变量的柱状图]

![img/C12624_02_12.jpg]

图 2.12:分类变量的条形图

与我们之前为数值特征创建的用于视觉自动化的函数类似,我们创建了一个简单的函数来探索分类特征的百分比分布。对函数的一些补充包括创建临时聚合数据集和一些额外的图表美化增强。我们添加了标签并将它们旋转 30 度,以便它们可以整齐地与图表对齐,其余保持不变。我们通过在categorical列上调用summary函数来获取频率统计。与数值列类似,我们首先使用summary函数探索分类列,然后使用定义的函数来可视化汇总的条形图。

探索job特征,我们可以看到 12 个不同的值,其中大多数记录为行政、蓝领和技师。总体而言,job类别似乎具有相当多样化的值分布。客户的受教育程度也有一系列多样化的值,大约 50%的值为高中和大学。对于表示客户是否以前在信贷中违约的default变量,大约 80%的值为no,大约 20%未知。这似乎不是很有用。最后,contact,它定义了用于活动通信的联系方式,显示 64%是通过移动电话进行的,其余是通过固定电话。

让我们继续并重复对下一组特征进行相同的分析。

练习 28:自动绘制剩余分类变量的图表

在这个练习中,我们将重用相同的函数来处理下一组四个分类变量。记住,你需要结合使用summary命令生成的频率统计和图表来解释值。

让我们执行以下步骤来完成练习:

  1. 首先,使用以下命令导入cowplot包:

    library(cowplot)
    
  2. 创建一个 DataFrame 对象df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 接下来,使用以下命令调用summary统计:

    summary(df[,c("loan","month","day_of_week","poutcome")])
    

    输出如下:

          loan           month       day_of_week        poutcome    
     no     :33950   may    :13769   fri:7827    failure    : 4252  
     unknown:  990   jul    : 7174   mon:8514    nonexistent:35563  
     yes    : 6248   aug    : 6178   thu:8623    success    : 1373  
                     jun    : 5318   tue:8090                       
                     nov    : 4101   wed:8134                       
                     apr    : 2632                                  
                     (Other): 2016
    
  4. 调用定义的函数来绘制直方图:

    plot_grid_categorical(df,c("loan","month","day_of_week","poutcome"),2)
    

    输出如下:

![图 2.13:自动绘制剩余分类变量的图表img/C12624_02_13.jpg

图 2.13:自动绘制剩余分类变量的图表

我们重用之前定义的函数来探索新的一组四个变量,就像我们探索之前的一组特征一样。

loan 变量表示客户是否有个人贷款。我们有大约 86.6% 的客户没有个人贷款,10.3% 有贷款,3.3% 未知。同样,month 变量表示活动电话执行的实际月份。我们看到大多数沟通是在 may 月份进行的,其次是 julaug。总体而言,month 特征似乎也是一个相当多样化的变量,具有良好的值分布。day_of_week 变量在每周的每一天都显示出一致的分布。poutcome 表示之前执行的活动的结果;绝大多数不存在,大约 3.3% 的成功,大约 10% 失败。

练习 29:探索最后一个剩余的分类变量和目标变量

最后,让我们探索最后一个剩余的分类变量和目标变量。由于两者都是分类变量,我们可以继续使用相同的函数进行探索。

对最后一个独立分类变量和因变量(也是分类变量)重复相同的过程:

  1. 首先,在导入所需的包并创建 DataFrame 对象后,使用以下命令调用摘要统计:

    summary(df[,c("y","housing")])
    

    输出如下:

       y            housing     
     no :36548   no     :18622  
     yes: 4640   unknown:  990  
                 yes    :21576
    
  2. 调用定义好的函数来绘制直方图:

    plot_grid_categorical(df,c("y","housing"),2)
    

    输出如下:

![图 2.14:按计数划分的住房直方图图片 C12624_02_14.jpg

图 2.14:按计数划分的住房直方图

如果我们仔细观察结果变量的分布,我们可以看到大多数客户对活动电话做出了负面回应。只有大约 11% 的整体活动基础对活动做出了积极回应。同样,如果我们查看 housing 变量,我们可以看到大约 50% 的客户有住房贷款。

总结来说,我们可以将我们的观察总结如下:

  • 该活动主要针对那些之前未曾接触过的新客户。

  • 大约 60% 的客户已婚,80% 在信用历史中没有违约。

  • 大约 50% 的客户有住房贷款,超过 80% 从未选择过个人贷款。

  • 该活动在五月份最为活跃,并在七月和八月展现了相当强的动力。

  • 活动的超过 60% 的沟通是通过手机进行的,并且超过 50% 的客户至少有高中文凭。

  • 总体而言,只有 11% 的活动电话获得了积极回应。

在完成所有数值和分类变量的单变量分析后,我们现在对数据传达的故事有了相当的了解。我们几乎理解了每个数据维度及其分布。让我们继续探索 EDA 的另一个有趣方面:双变量分析。

双变量分析

双变量分析中,我们将分析扩展到同时研究两个变量。在我们的用例中,我们有大约 20 个独立变量。实际上,我们可以研究所有 20 个变量的排列组合,但在这个章节中我们不会达到那个程度。在我们的用例中,我们更感兴趣的是研究导致活动表现不佳的所有因素。因此,我们的主要焦点将是执行双变量分析,研究所有独立变量与我们的目标变量之间的关系。再次强调,根据变量的类型,我们将有不同的视觉或分析技术来分析两个变量之间的关系。可能的组合是数值与数值,以及数值与分类。鉴于我们的目标变量是分类变量,我们可能需要探索列表中两个独立变量之间的关系,以研究两个数值变量之间的关系。让我们开始吧。

研究两个数值变量之间的关系

为了理解我们如何研究两个数值变量之间的关系,我们可以利用散点图。这是一个二维的数据可视化,其中每个变量都沿着其长度绘制在轴上。通过研究可视化中的趋势,可以轻松地识别变量之间的关系。让我们在下面的练习中看看一个例子。

练习 30:研究雇员方差率与雇员数量之间的关系

让我们研究雇员方差率和雇员数量之间的关系。理想情况下,随着方差率的增加,雇员数量应该增加。

执行以下步骤来完成练习:

  1. 首先,使用以下命令导入ggplot2包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 现在,使用以下命令绘制散点图:

    ggplot(data=df,aes(x=emp.var.rate,y=nr.employed)) + geom_point(size=4) + 
    ggtitle("Scatterplot of Employment variation rate v/s Number of Employees")
    

    输出如下:

图 2.15:就业变动与雇员数量的散点图

图 2.15:就业变动与雇员数量的散点图

我们使用相同的基函数ggplot,并添加了一个新的包装器来处理散点图。ggplot中的geom_point函数提供了使用散点图所需的结构。

我们可以看到一个整体上升的趋势,即随着就业方差率的增加,我们也看到雇员数量也在增加。nr.employed中的点数较少是由于重复的记录。

研究分类变量与数值变量之间的关系

让我们先回顾一下研究数值变量和分类变量之间关系的方法,并讨论执行该方法的步骤。

在本节中,我们将讨论我们可以用于总结数据的不同聚合度量。到目前为止,我们已经使用了avg,但更好的方法是将avgminmax和其他度量结合使用。

练习 31:研究 y 和年龄变量之间的关系

我们有一个分类的因变量和九个数值变量要探索。为了从小处着手,我们首先将探索我们的目标yage之间的关系。为了研究分类和数值变量之间的关系,我们可以选择一种简单的分析技术,即计算每个目标结果中的平均年龄;如果我们看到明显的差异,我们可以从观察中得出见解。

在这个练习中,我们将计算每个目标结果中的平均年龄,并计算每个桶中的记录数,然后进行可视化表示。

执行以下步骤:

  1. 首先,使用以下命令导入ggplot2包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令使用bank-additional-full.csv文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 创建一个temp对象并使用以下命令存储值:

    temp <- df %>% group_by(y) %>% 
                               summarize(Avg.Age = round(mean(age),2),
                                         Num.Records = n())
    
  4. 打印存储在temp对象中的值:

    print(temp)
    

    输出如下:

    # A tibble: 2 x 3
      y     Avg.Age Num.Records
      <fct>   <dbl>       <int>
    1 no       39.9       36548
    2 yes      40.9        4640
    
  5. 现在,使用ggplot命令创建一个图表:

    ggplot(data= temp, aes(x=y, y=Avg.Age)) + 
           geom_bar(stat="identity",fill="blue",alpha= 0.5) +   #Creates the bar plot
           geom_text(label=temp$Avg.Age,vjust=-0.3)+  #Adds the label
           ggtitle(paste("Average Age across target outcome"))  #Creates the title
    

    输出如下:

![图 2.16:目标结果中平均年龄的直方图图片

图 2.16:目标结果中平均年龄的直方图

第一行代码创建了一个临时聚合数据集,该数据集总结了每个类别的平均年龄和记录数。所使用的绘图功能与之前的视觉类似。我们通过使用geom_bar扩展ggplot函数来渲染条形图。

我们可以看到两个结果之间几乎没有差异。我们没有看到任何有趣的模式。

注意

在双变量分析中,在得出任何有趣的模式作为见解之前,我们需要小心谨慎。在许多情况下,由于数据的偏斜分布,这些模式看起来会令人惊讶地有趣。

让我们继续研究下一组变量。

练习 32:研究平均值与 y 变量之间的关系

在这个练习中,我们将研究下一组变量之间的关系:averagey

执行以下步骤以完成练习:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 接下来,使用以下命令创建plot_bivariate_numeric_and_categorical对象:

    plot_bivariate_numeric_and_categorical <- function(df,target,list_of_variables,ncols=2){
        target<-sym(target) #Defined for converting text to column names
        plt_matrix <- list()
        i<-1
    for(column in list_of_variables){
            col <-sym(column) #defined for converting text to column name
            temp <- df %>% group_by(!!sym(target)) %>% 
                           summarize(Avg.Val = round(mean(!!sym(col)),2))
            options(repr.plot.width=12, repr.plot.height=8) #Defines plot size
               plt_matrix[[i]]<-ggplot(data= temp, aes(x=!!sym(target), y=Avg.Val)) + 
               geom_bar(stat="identity",fill="blue",alpha= 0.5) +   
               geom_text(label=temp$Avg.Val,vjust=-0.3)+  #Adds the labels
               ggtitle(paste("Average",column,"across target outcomes"))  #Creates the title 
                i<-i+1
        }
        plot_grid(plotlist = plt_matrix,ncol=ncols)
    }
    
  3. 现在,打印记录在目标结果中的分布:

    print("Distribution of records across target outcomes-")
    print(table(df$y))
    

    输出如下:

    [1] "Distribution of records across target outcomes-"
       no   yes 
    36548  4640
    
  4. 现在,使用以下命令为定义的变量绘制直方图:

    plot_bivariate_numeric_and_categorical(df,"y",c("campaign","pdays","previous","emp.var.rate"),2)
    

    输出如下:

![图 2.17:平均值与 y 变量的直方图图片

图 2.17:平均值与 y 变量的直方图

为了自动化数据探索任务,以便进行分类变量和数值变量之间的双变量分析,我们定义了一个类似于上一个练习中定义的函数。我们还使用了sym函数,这将帮助我们使用函数中的动态列名。使用!!sym(column)将字符串转换为真正的列名,类似于传递实际值。上一个函数首先对感兴趣的变量中的目标平均值进行聚合。然后plot函数使用这些信息绘制出目标结果平均值的条形图。

在双变量分析中,在得出具体见解之前,仔细验证观察到的模式非常重要。在某些情况下,异常值可能会扭曲结果,从而得出错误的结论。此外,特定模式的记录较少也可能是一个风险模式。始终建议收集所有观察到的见解,并使用额外的广泛 EDA 或统计技术进行验证,以确定其显著性。

在这里,我们没有看到任何显著的结果可以得出结论。在campaign变量中,成功活动中进行的平均联系次数略低,但差异太小,无法得出任何可能的结论。pdays,表示上次联系以来的天数,在目标的结果之间显示出很大的差异。

然而,这种差异纯粹是因为大多数客户在上一个活动中没有联系。所有这些记录的值都设置为 999。对于previous也是如此;尽管两者之间有相当大的差异,但大多数客户在当前活动中是第一次被联系。然而,就业方差率却显示出反直觉的结果。我们实际上预计当结果为yes时,方差率应该更高,但我们看到的是相反的情况。这听起来很有趣,我们暂时将这个见解记录下来,稍后再回来进行更多验证,然后再得出任何结论。

让我们继续研究下一组将要被研究的分类因变量。

练习 33:研究 cons.price.idx、cons.conf.idx、euribor3m 和 nr.employed 变量之间的关系

让我们继续研究下一组将要被研究的分类因变量。在这个练习中,我们将使用直方图来探索cons.price.idxcons.conf.idxeuribor3mnr.employed与目标变量y之间的关系。

  1. 导入所需的库并创建 DataFrame 对象。

  2. 接下来,创建一个plot_bivariate_numeric_and_categorical函数并绘制直方图:

    plot_bivariate_numeric_and_categorical(df,"y",
                   c("cons.price.idx","cons.conf.idx", "euribor3m", "nr.employed"),2)
    

    输出如下:

![图 2.18:cons.price.idx、cons.conf.idx、euribor3m 和 nr.employed 变量的直方图img/C12624_02_18.jpg

图 2.18:cons.price.idx、cons.conf.idx、euribor3m 和 nr.employed 变量的直方图

再次强调,在大多数情况下,我们没有看到任何明显的模式。然而,euribor3m 变量在 yesno 结果的平均值之间显示出一些良好的差异,这再次看起来是反直觉的。我们理想上期望利率越高,银行存款就越多。因此,让我们记下这个见解,并在稍后验证它。

接下来,让我们现在探索两个分类变量之间的关系。

研究两个分类变量之间的关系

为了研究两个分类变量之间存在的关联和模式,我们可以首先探索变量每个类别的频率分布。任何结果的高浓度可能是一个潜在的见解。最有效的方式来可视化这一点是使用堆积柱状图。

堆积柱状图将帮助我们观察目标变量在多个分类变量中的分布。分布将揭示某个分类变量中的特定类别是否主导了目标变量 y。如果是这样,我们可以进一步探索其对问题的潜在影响。

在接下来的几个练习中,我们将使用堆积柱状图探索目标变量 y 上的各种分类变量。我们将绘制绝对计数和百分比,以更好地理解分布情况。

练习 34:研究目标变量 y 和婚姻状况变量之间的关系

在这个练习中,我们将展示使用纯频率计数研究两个分类变量之间的关系,然后展示其不便之处。

为了简单起见,让我们从探索目标变量 y婚姻状况 之间的关系开始。

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个名为 df 的 DataFrame 对象,并使用以下命令使用 bank-additional-full.csv 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 接下来,创建一个 temp 聚合数据集:

    temp <- df %>% group_by(y,marital) %>% summarize(Count = n()) 
    
  4. 定义绘图大小,如图所示:

    options(repr.plot.width=12, repr.plot.height=4)
    
  5. 绘制频率分布图:

    ggplot(data = temp,aes(x=marital,y=Count,fill=y)) + 
           geom_bar(stat="identity") + 
           ggtitle("Distribution of target 'y' across Marital Status")
    

    输出如下:

    图 2.19:使用 ggplot 研究目标变量 y 和婚姻状况变量之间的关系

    图 2.18:cons.price.idx、cons.conf.idx、euribor3m 和 nr.employed 变量的直方图

    图 2.19:使用 ggplot 研究目标变量 y 和婚姻状况变量之间的关系

    我们首先使用 group_by 函数对分类列进行聚合。这将帮助我们计算每个类别组合的交叉频率计数。现在我们使用这个临时数据集来绘制独立变量的频率分布。

    如我们所见,已婚客户的 yes 频率最高,但这可能仅仅是因为已婚客户数量较多。为了更好地理解这种关系,我们可以进一步使用带有百分比分布的堆积柱状图来细分。

  6. 创建一个 temp 聚合数据集:

    temp <- df %>% group_by(y,marital) %>% 
                   summarize(Count = n()) %>% 
                   ungroup() %>%  #This function ungroups the previously grouped dataframe
                   group_by(marital) %>%
                   mutate(Perc = round(Count/sum(Count)*100)) %>%
                   arrange(marital)
    
  7. 使用 options 方法定义绘图大小:

    options(repr.plot.width=12, repr.plot.height=4)
    
  8. 使用 ggplot 方法绘制百分比分布图:

    ggplot(data = temp,aes(x=marital,y=Perc,fill=y)) + 
        geom_bar(stat="identity") + 
        geom_text(aes(label = Perc), size = 5, hjust = 0.5, vjust = 0.3, position = "stack") + 
        ggtitle("Distribution of target 'y' percentage across Marital Status")
    

    输出如下:

![图 2.20:目标 y 百分比在婚姻状态中的分布img/C12624_02_20.jpg

图 2.20:目标 y 百分比在婚姻状态中的分布

与之前的图表相比,我们现在可以看到一些反直觉的结果。在结果归一化后,我们发现 single 客户对活动的响应比已婚客户更积极。对于 unknown 也是如此,但由于值的不确定性和记录数量极低,我们应该忽略这一点。我们不能直接得出单身客户在响应活动方面更有效的结论,但我们可以稍后验证这一点。

练习 35:研究工作和教育变量之间的关系

在这个练习中,我们将加速我们的探索。让我们构建一个自定义函数,我们可以在这个函数中结合两个图表,即频率分布以及百分比分布,用于分类变量的双变量分析。

执行以下步骤:

  1. 首先,使用以下命令导入 ggplot2 包:

    library(ggplot2)
    
  2. 创建一个 DataFrame 对象,df,并使用以下命令加载 bank-additional-full.csv 文件:

    df <- read.csv("/Chapter 2/Data/bank-additional/bank-additional-full.csv",sep=';')
    
  3. 创建一个 temp 聚合数据集:

    plot_bivariate_categorical <-  function(df, target, list_of_variables){
        target <- sym(target) #Converting the string to a column reference
        i <-1 
        plt_matrix <- list()
        for(column in list_of_variables){
            col <- sym(column) 
            temp <- df %>% group_by(!!sym(target),!!sym(col)) %>% 
               summarize(Count = n()) %>% 
               ungroup() %>% #This fucntion ungroups the previously grouped dataframe
               group_by(!!sym(col)) %>%
               mutate(Perc = round(Count/sum(Count)*100)) %>%
               arrange(!!sym(col))
    
  4. 定义绘图大小:

    options(repr.plot.width=14, repr.plot.height=12)
    
  5. 使用频率分布绘制图表:

        plt_matrix[[i]]<- ggplot(data = temp,aes(x=!!sym(col),y=Count,fill=!!sym(target))) + 
            geom_bar(stat="identity") + 
            geom_text(aes(label = Count), size = 3, hjust = 0.5, vjust = -0.3, position = "stack") + 
            theme(axis.text.x = element_text(angle = 90, vjust = 1)) + #rotates the labels
            ggtitle(paste("Distribution of target 'y'  frequency across",column))
        i<-i+1
    
  6. 绘制百分比分布图:

        plt_matrix[[i]] <- ggplot(data = temp,aes(x=!!sym(col),y=Perc,fill=!!sym(target))) + 
            geom_bar(stat="identity") + 
            geom_text(aes(label = Perc), size = 3, hjust = 0.5, vjust = -1, position = "stack") + 
            theme(axis.text.x = element_text(angle = 90, vjust = 1)) + #rotates the labels
            ggtitle(paste("Distribution of target 'y' percentage across",column))
        i <- i+1
        }
        plot_grid(plotlist = plt_matrix, ncol=2)
    }
    
  7. 使用以下命令绘制 plot_bivariate_categorical 图:

    plot_bivariate_categorical(df,"y",c("job","education"))
    

    输出如下:

    ![图 2.21:研究工作和教育变量之间的关系 img/C12624_02_21.jpg

    图 2.21:研究工作和教育变量之间的关系

    我们使用相同的原则来定义绘制图表的函数。这里的额外区别是每个组合两个图表。第一个(左侧)是类别组合的频率图,右侧的图表展示了百分比分布(按类别归一化)的视觉展示。同时研究这两个图表有助于更有效地验证结果。使用 ungroup 函数创建临时聚合数据集有额外的步骤。这是用来启用目标结果在独立变量的分类水平中的相对百分比分布,即 ymarital 的每个水平内的分布。

    如果我们观察前一个输出图的结果,我们可以看到,活动的最高响应率来自学生和退休专业人士,但这也存在一个警告。我们发现这两个类别与其他类别相比观察到的数量要少得多。因此,在得出进一步结论之前,我们需要额外的验证。因此,我们也将这个洞察力记录下来。从教育水平来看,我们没有看到任何有趣的趋势。尽管文盲客户的响应率很高,但观察到的数量太少,无法得出任何有意义的结论。

  8. 让我们来看看信用违约和住房贷款类别:

    plot_bivariate_categorical(df,"y",c("default","housing"))
    

    输出结果如下:

    图 2.22:研究默认和住房变量之间的关系

    图 2.22:研究默认和住房变量之间的关系
  9. 再次,我们没有看到任何有趣的趋势。让我们继续探索个人贷款和联系模式:

    plot_bivariate_categorical(df,"y",c("loan","contact"))
    

    输出结果如下:

图 2.23:研究贷款和联系变量之间的关系

图 2.23:研究贷款和联系变量之间的关系

在这里,我们可以看到联系方式的模式存在一个有趣的趋势。当活动沟通模式是手机而非固定电话时,通常会有更高的响应率。让我们也记录这个趋势,并进一步验证。

我鼓励您探索目标变量与剩余的依赖性分类变量(月份、星期几和活动的先前结果)之间的关系。

多变量分析

多变量分析是研究两个以上变量之间关系的过程;本质上,一个因变量和多个自变量。双变量分析是多变量分析的一种形式。有几种重要的多变量分析方法,但为了限制本章的范围,我们暂时跳过细节。在接下来的几章中,我们将更详细地研究线性回归和逻辑回归,这两种都是流行的多变量分析方法。

在多元分析中,一些最常用的技术如下:

  • 多元线性回归(研究多个自变量对一个数值/连续目标变量的影响)

  • 逻辑回归(研究多个自变量对一个分类目标变量的影响)

  • 因素分析

  • 多元方差分析(MANOVA)

使用统计测试验证洞察力

在 EDA 的整个过程中,我们已经收集并记录了一些有趣的模式以供进一步验证。现在是测试我们之前观察到的模式是否真正有效,或者只是由于随机机会而看似有趣的时候了。进行这种验证最有效和最直接的方法是通过执行一系列统计测试并测量模式的统计显著性。我们在可用的测试集中有很多选项可供选择。这些选项取决于独立变量和依赖变量的类型。以下是一个实用的参考图,解释了我们可以执行以验证观察到的模式的各种统计测试类型:

图 2.24:验证依赖和独立变量

图 2.24:验证依赖和独立变量

让我们把所有有趣的模式收集到一个地方:

  • 当员工差异率低时,活动的结果更有可能为“是”。

  • 当欧元利率低时,活动的结果更有可能为“是”。

  • 单一客户有更高的可能性对活动做出积极回应。

  • 学生和退休客户更有可能对活动做出积极回应。

  • 移动电话联系人有更高的可能性对活动做出积极回应。

如果你尝试对这些假设进行分类,我们可以看到在所有情况下我们都有一个分类的依赖变量。因此,我们应该使用卡方检验或逻辑回归检验来验证我们的结果。

让我们逐一进行这些测试。

分类依赖变量和数值/连续独立变量

假设 1 和 2 有一个连续的独立变量。参考前一小节的图,我们将选择卡方检验。在假设检验的过程中,我们首先定义一个零假设和一个备择假设。从一个否定的角度开始,即假设零假设是我们不希望发生的事情。假设检验检查观察到的模式是由于随机机会发生的可能性,或者是否有关于观察的确定性。这个度量量化为概率。如果零假设显著性的概率小于 5%(或合适的截止值),我们拒绝零假设,并确认备择假设的有效性。

让我们开始吧;对于假设 1,我们定义以下内容:

  • 零假设:活动的结果与员工差异率没有关系。

  • 备择假设:活动的结果与员工差异率有关。

我们使用简单的逻辑回归测试来测试我们零假设的有效性。我们将在接下来的章节中更详细地讨论这个话题。现在,我们将快速执行一个简单的检查来测试我们的假设。以下练习利用了 R 内置的逻辑回归函数。

练习 36:对分类因变量和连续自变量进行假设 1 测试

要对分类因变量和连续自变量进行假设测试,我们将使用glm()函数拟合逻辑回归模型(更多内容请参阅第五章分类)。这个练习将帮助我们统计检验一个分类因变量(例如,y)是否与一个连续自变量(例如)有任何关系,

emp.var.rate

执行以下步骤以完成练习:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 首先,将因变量转换为factor类型:

    df$y <- factor(df$y)
    
  3. 接下来,执行逻辑回归:

    h.test <- glm(y ~ emp.var.rate, data = df, family = "binomial")
    
  4. 打印测试摘要:

    summary(h.test)
    

    输出如下:

    Call:
    glm(formula = y ~ emp.var.rate, family = "binomial", data = df)
    Deviance Residuals: 
        Min       1Q   Median       3Q      Max  
    -1.0047  -0.4422  -0.3193  -0.2941   2.5150  
    Coefficients:
                 Estimate Std. Error z value Pr(>|z|)    
    (Intercept)  -2.33228    0.01939 -120.31   <2e-16 ***
    emp.var.rate -0.56222    0.01018  -55.25   <2e-16 ***
    ---
    Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    (Dispersion parameter for binomial family taken to be 1)
        Null deviance: 28999  on 41187  degrees of freedom
    Residual deviance: 25597  on 41186  degrees of freedom
    AIC: 25601
    Number of Fisher Scoring iterations: 5
    

我们将目标变量y转换为factor类型(如果它尚未如此)。我们使用 R 提供的glm函数进行逻辑回归。glm函数还执行其他形式的回归,我们指定family = 'binomial'参数以将函数用作逻辑回归。函数的第一个位置中的公式定义了因变量和自变量。

输出中有很多结果共享。现在我们将忽略其中大部分,只关注最终输出。提供的结果之一是显著性概率,这证实了我们的零假设为真的可能性小于2e-16,因此我们可以拒绝它。因此,目标结果与员工方差率有统计学上的显著关系,并且,正如我们所看到的,随着率的降低,竞选转化的可能性更高。

类似地,让我们为第二个假设重复相同的测试。我们定义以下内容:

  • 零假设:竞选结果与欧元利率之间没有关系。

  • 备择假设:竞选结果与欧元利率之间存在关系。

练习 37:对分类因变量和连续自变量进行假设 2 测试

再次,我们将使用逻辑回归来统计检验目标变量y与自变量之间是否存在关系。在这个练习中,我们将使用euribor3m变量。

执行以下步骤:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 首先,将因变量转换为factor类型:

    df$y <- factor(df$y)
    
  3. 接下来,执行逻辑回归:

    h.test2 <- glm(y ~ euribor3m, data = df, family = "binomial")
    
  4. 打印测试摘要:

    summary(h.test2)
    

    输出如下:

    Call:
    glm(formula = y ~ euribor3m, family = "binomial", data = df)
    Deviance Residuals: 
        Min       1Q   Median       3Q      Max  
    -0.8568  -0.3730  -0.2997  -0.2917   2.5380  
    Coefficients:
                 Estimate Std. Error z value Pr(>|z|)    
    (Intercept) -0.472940   0.027521  -17.18   <2e-16 ***
    euribor3m   -0.536582   0.009547  -56.21   <2e-16 ***
    ---
    Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    (Dispersion parameter for binomial family taken to be 1)
        Null deviance: 28999  on 41187  degrees of freedom
    Residual deviance: 25343  on 41186  degrees of freedom
    AIC: 25347
    Number of Fisher Scoring iterations: 5
    

专注于前面的输出,我们可以确认我们可以拒绝零假设并接受备择假设。因此,目标结果与欧元利率有统计学上的显著关系,并且,正如我们所看到的,随着利率的降低,竞选转化的可能性更高。

分类因变量和分类自变量

接下来,让我们看一下第三个假设。为了测试分类因变量和分类自变量之间的关系,我们可以使用卡方检验。

对于假设 3,我们定义如下:

  • 零假设:活动的结果与从未结婚的客户之间没有关系。

  • 备择假设:活动的结果与从未结婚的客户之间存在关系。

在以下练习中,我们将利用 R 的卡方检验函数来验证假设。

练习 38:对分类因变量和分类自变量进行假设 3 的测试

在这个练习中,我们将使用卡方检验进行统计分析。我们使用卡方检验是因为独立变量和因变量都是分类的,尤其是在测试y与婚姻状况之间的关系时。

执行以下步骤:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 首先,将因变量转换为factor类型:

    df$y <- factor(df$y)
    
  3. single客户创建一个标志:

    df$single_flag <- as.factor(ifelse(df$marital == "single","single","other"))
    
  4. 创建一个sample对象并打印其值:

    sample <- table(df$y, df$single_flag)
    print(sample)
    

    输出如下:

      other single
    no  26600   9948
    yes  3020   1620
    
  5. 执行卡方检验:

    h.test3 <- chisq.test(sample)
    
  6. 打印测试摘要:

    print(h.test3)
    

    输出如下:

    Pearson's Chi-squared test with Yates' continuity correction
    data:  sample
    X-squared = 120.32, df = 1, p-value < 2.2e-16
    

我们首先为这个测试创建一个新的变量/标志,其中我们定义客户是否为single。由于我们专门定义了目标和客户的single婚姻状况之间的关系,我们屏蔽了婚姻状况中的所有其他类别。

table命令创建了一个新的 DataFrame,其中包含每个个体类别之间的简单频率分布。最后,我们使用这个 DataFrame 来执行卡方检验。

如我们所见,零假设为真的 p 值或概率远小于 5%。因此,我们可以接受我们的备择假设,这证实了事实,即活动的结果是由单个客户而不是其他客户积极影响的。

接下来,让我们快速看一下我们的第 4 个和第 5 个假设的有效性。

对于第 4 个和第 5 个假设,我们定义如下:

  • 零假设:活动的结果与学生或退休的客户之间没有关系。活动的结果与使用的联系方式没有关系。

  • 备择假设:活动的结果与学生或退休的客户之间没有关系。活动的结果与使用的联系方式有关。

练习 39:对分类因变量和分类自变量进行假设 4 和 5 的测试

再次使用卡方检验来统计地检查目标变量y、分类自变量job_flagcontact之间是否存在关系。

执行以下步骤:

  1. 导入所需的库并创建 DataFrame 对象。

  2. 首先,将因变量转换为factor类型:

    df$y <- factor(df$y)
    
  3. 准备自变量:

    df$job_flag <- as.factor(ifelse(df$job %in% c("student","retired"),as.character(df$job),"other"))
    df$contact <- as.factor(df$contact)
    
  4. 创建一个名为sample4的对象并打印其值:

    sample4 <- table(df$y, df$job_flag)
    print("Frequency table for Job")
    print(sample4)
    

    输出如下:

    [1] "Frequency table for Job"
      other retired student
    no  34662    1286     600
    yes  3931     434     275
    
  5. 对第 4 个假设进行测试:

    h.test4 <- chisq.test(sample4)
    
  6. 打印第 4 个假设的测试摘要:

    print("Hypothesis #4 results")
    print(h.test4)
    

    输出如下:

    [1] "Hypothesis #4 results"
    Pearson's Chi-squared test
    data:  sample4
    X-squared = 736.53, df = 2, p-value < 2.2e-16
    
  7. 现在,创建一个新的sample5对象并打印其值:

    print("Frequency table for Contact")
    sample5 <- table(df$y, df$contact)
    print(sample5)
    

    输出如下:

    [1] "Frequency table for Contact"
      cellular telephone
    no     22291     14257
    yes     3853       787
    
  8. test5变量进行测试:

    h.test5 <- chisq.test(sample5)
    
  9. 打印第 5 个假设的测试摘要:

    print("Hypothesis #5 results")
    print(h.test5)
    

    输出如下:

    [1] "Hypothesis #5 results"
    Pearson's Chi-squared test with Yates' continuity correction
    data:  sample5
    X-squared = 862.32, df = 1, p-value < 2.2e-16
    

我们可以看到结果已经得到了验证。我们还可以看到学生和退休客户之间以及与活动相关的通信方式之间肯定存在一种关系。

汇总见解 – 精炼问题的解决方案

现在我们已经遍历了 EDA 的长度和宽度。在不同的部分,我们以不同的深度研究了数据。现在我们有了数据探索问题的有效答案,我们可以再次与最初定义的问题接触。如果你还记得问题陈述中的复杂性问题部分,我们问的是“是什么因素导致了活动的表现不佳”。嗯,我们现在基于我们在双变量分析中发现的模式,并通过统计测试验证了答案。

将所有与正确故事验证的假设整理在一起,为我们的问题带来了解决方案。花些时间仔细研究每个假设测试结果,以编织故事。每个假设都告诉我们一个自变量是否与一个因变量有关系。

摘要

在本章中,我们通过一个实际案例研究探索了 EDA,并遍历了商业问题。我们首先理解了执行数据科学问题的整体流程,然后使用行业标准框架定义我们的商业问题。通过使用案例与适当的问题和复杂性相结合,我们理解了 EDA 在为问题设计解决方案中的作用。在探索 EDA 之旅中,我们研究了单变量、双变量和多变量分析。我们使用分析技术和视觉技术相结合的方法进行数据分析。通过这种方式,我们探索了用于可视化的 R 包,即ggplot,以及通过dplyr进行数据整理的一些包。我们还通过统计测试验证了我们的见解,并最终整理了记录的见解,以便与原始问题陈述回溯。

在下一章中,我们将为各种机器学习算法奠定基础,并深入讨论监督学习。