R数据挖掘实战
上QQ阅读APP看书,第一时间看更新

1.5 R语言的基本概念

既然读者已经安装了R语言并且选择了自己喜爱的R语言开发环境,接下来是时候体验一下,并获得R语言的一些基础知识了。本节将介绍R语言的主要构建模块,这些构建模块会应用于本书的所有数据挖掘算法之中。更具体地说,通过在交互式控制台上执行基本操作并保存第一个R语言脚本,读者将学会如何创建和操作以下构建模块(见图1-6)。

向量(Vector):用于存储有序的一个或多个值。

列表(List):一种数据集合,用于存储向量或者R语言中其他类型的对象。

数据帧(Dataframe,也称数据框):可被看作由很多向量组成的二维列表,数据帧中所有向量内元素的数量是相同的。

函数(Function):一组作用于向量、列表和数据帧的指令集合,通过操作这些对象来生成新的结果。

图1-6

接下来,讲解如何使用自定义函数以及如何安装扩展包来使用R语言的更多功能。如果上述所提及的概念让读者感到不知所措,不必担心,通过后续的学习,读者一定能对以上构建模块及其概念达到非常熟悉的程度。

1.5.1 R语言入门

在了解R这门强大语言的入门知识之前,先来看看R语言的基本操作,具体如下。

在R语言控制台上执行一些基本操作。

创建并保存R语言脚本。

在R语言控制台执行R语言脚本。

1.通过R语言控制台交互式地执行脚本

打开你最喜欢的IDE(本书使用RStudio),通过闪烁的光标,能够找到交互式控制台。在找到控制台之后,输入如下字符并按“Enter”键,即可向控制台提交命令:

2+2

提交的命令的执行结果会在下一行自动输出:

4

上述命令及输出只是一个简单的例子,后续将讨论更加复杂的数学运算。

读者虽然在控制台上可以很方便地、交互式地测试小代码块,但需要注意的是,一旦终止了R语言会话(如关闭IDE),控制台上执行的所有操作都会丢失。虽然一些IDE(如RStudio)会记录控制台历史操作,但它只是追溯历史操作的一个线索,而不是一种有效地存储代码的方式(见图1-7)。

图1-7

后面将介绍存储控制台命令历史记录的正确方法。同时,为了完整性考虑,在这里说明一下:R语言可以执行所有基本的数学运算,可以使用的运算符包括+、−、*、/、^(幂运算)。

2.创建并保存R语言脚本

R语言脚本是存储或多或少的R语言代码的一种文件,其优点是可以存储和显示可重复执行,或在脚本外部调用执行的一组结构化的指令(更多详情见后文)。在IDE中,可以找到用于新建脚本的控件或者按钮;单击该控件或按钮,可生成一个以.R为扩展名的文件;打开该文件,就可以开始编写R语言代码了。如果没有找到类似的控件或按钮,那么读者可能需要考虑更换一款IDE,或者尝试在R语言控制台上运行以下命令:

file.create("my_first_script.R")

现在就开始在文件中编写一些代码,按照惯例,使用“大名鼎鼎”的句子“hello world”(你好,世界)来测试脚本吧。如何才能输出这两个神奇的单词呢?读者只需告诉R语言执行输出即可,如图1-8所示,具体如下:

print("hello world")

图1-8

再重复一遍,当前这行代码只是“热身”,后续会介绍更多复杂的场景,所以读者不必担心本书内容过于浅显。

在往下阅读之前,请读者在文件中再添加一行内容,这次要添加的不是代码,而是注释:

# my dear interpreter, please do not execute this line, it is just a comment

实际上,注释与软件开发是紧密联系的。读者可能很轻易地猜到,这样的注释语句是不会被解释器执行的,因为解释器会忽略所有“#”之后的内容。虽然注释会被解释器忽略,但它对程序员来说却非常宝贵,尤其对于写完脚本之后一个月,再回头来看脚本的程序员而言。注释一般用于标注代码的基本原理、条件和目标,以便于程序员理解代码的作用范围是什么、为什么执行某些操作,以及必须满足哪些条件才能让代码正常运行。

请注意,注释可以和代码写在同一行,如下面的例子所示:

print("hello world") # dear interpreter, please do not execute this comment

在IDE上找到“保存”按钮,单击该按钮就可以保存脚本文件了。保存脚本文件时需要输入文件名,可以将名字设置为“my_first_script.R”,因为在接下来的内容里会用到它。

3.执行R语言脚本

随着编码能力的逐步提升,读者就更有可能逐步地将数据分析的不同部分存储到不同的脚本中,并从终端或者一个主脚本中按顺序调用它们。因此,从一开始就正确地掌握脚本文件的存储操作是至关重要的。而且完整地执行脚本是检查代码错误(即bug)的好方法。将数据分析过程存储在脚本中,能够方便其他感兴趣的用户复用代码,从而验证读者的分析结果的正确性——这是非常理想的一大特性。

现在,请读者尝试执行之前所创建的脚本。在R语言环境下,通过调用source()函数来执行一个R语言脚本。正如在本书后面部分会更深入介绍的那样,函数是一组指令的集合,它通常接收一个或多个输入,并产生一个输出。函数的输入被称为参数,函数的输出被称为值。在当前的例子中,将指定一个唯一的参数,即脚本文件参数。当前的例子使用之前保存的文件名作为参数,执行如下命令:

source("my_first_script.R")

执行这个命令的过程是怎样的呢?可以想象解释器一边读取每行代码,一边说:“来看看这个‘my_first_script’文件中的内容。好的,这里面有一条R命令——print("hello world"),执行它看看会发生什么。”虽然上述引用的这些言语是虚构的,但解释器确实就是这样运行的。解释器会查找指定的文件,读取文件内容,并执行存储在其中的R语言命令。当前的例子会在控制台上产生如下输出:

hello world

现在是时候开始真正学习R语言入门知识了,让我们先从向量开始吧!

1.5.2 向量

什么是向量?哪里会使用到向量?“向量”这个术语来自代数领域,但在R语言的世界里,向量并没有那么复杂,读者可以简单地将它看成由同一种数据类型的值所组成的有序序列。不同顺序的序列是不同的对象,如图1-9所示。

图1-9

如何在R语言中创建一个向量呢?答案是可以通过c()函数来创建,具体如下面的语句所示:

c(100,20,40,15,90)

上述语句创建的是常规的向量,只要在R语言控制台输出之后,它就失效了。如果想让该向量在R语言环境中保存下来,需要创建一个变量。可以简单地通过赋值操作来完成:

vector <- c(100,20,40,15,90)

只要执行这个命令,R语言环境中就会增加一个新的、类型为向量的对象。向量实际上有什么用途呢?作为使用R语言进行开发的基础,其每一个输入以及R语言产生的每一个输出都可以被简化成一个向量。例如,本书会将数据统计分析的结果存储在向量中,也会用向量来表示模型所遵循的概率分布。

需要注意的是,到目前为止,虽然读者在本书中只看到了一个数值向量,但实际上,读者可以定义包含表1-1所示的所有类型数据的向量。

表1-1

甚至还可以定义混合类型的向量:

mixed_vector <- c(1, TRUE, "text here")

确切地说,混合类型的向量最终会被强制转换成能包含所有其他类型的向量类型,就像刚刚例子中会全部转换为字符类型的向量。有关细节不在这里多加讨论,以免读者混淆。

现在,读者已经知道如何创建一个向量并保存该向量了。那么,如何调用并显示该向量所存储的内容呢?一般来说,调用一个对象只需要使用它的名字即可。可以在控制台上输入刚刚创建的向量的名字“mixed_vector”并按“Enter”键提交,结果如下:

[1] "1" "TRUE" "text here"

1.5.3 列表

既然读者已经知道了“什么是向量”,就很容易理解列表了,它是包含对象的容器。列表容器内的对象可以是其他列表,甚至可以是数据帧。在R语言环境中,用列表来存储对象很方便。例如,很多统计函数用列表来存储运行结果。

请读者看看如何使用列表,代码如下:

regression_results <- lm(formula = Sepal.Length ~ Species, data = iris)

IRIS数据集是非常有名的R语言预加载数据集,它包含在每个R语言基础版本中。上面这行命令表示基于IRIS数据集拟合一个回归模型(后文会详细讲解回归模型的细节),该模型试图解释特定种类的鸢尾花的萼片长度规律。

现在来看看 regression_results对象,正如之前说过的,它存储了回归模型拟合的结果。如果要查看指定对象的类型,可以使用mode()函数,该函数将对象名称作为参数x的值,如下所示:

mode(x = regression_results)

运行结果如下:

list

1.创建列表

请读者查看之前的代码,看看通常都是如何创建列表的呢?在创建列表时,读者总是会用到赋值运算符“<-”,该运算符在创建向量时也会用到。两者之间的区别是,创建列表时使用的函数不同。创建列表时不是使用c()函数,而是使用list()函数。如下面例子所示,请读者尝试创建两个向量,然后将它们合并到一个列表中:

first_vector <- c("a","b","c")
  second_vector <- c(1,2,3)
  vector_list <- list(first_vector, second_vector)

2.获取列表子集

如果读者想定位列表中的特定对象,应该怎么做呢?这时需要使用[[]]运算符,用来指定要获取对象的层级。例如,想获取vector_list列表中的第二个向量,可以使用如下代码:

vector_list[[2]]

输入上述代码并按“Enter”键提交,会返回如下结果:

[1] 1 2 3

读者也许会想,能否获取列表中的单个对象的单个元素呢?答案是肯定的。例如,想要获取second_vector中的第三个元素,只需要再使用一次[[]]运算符即可,代码如下:

vector_list[[2]][[3]]

输入上述代码并按“Enter”键提交,会返回如下结果:

[1] 3

1.5.4 数据帧

如果满足下面的规则,数据帧可被简单地看成列表。

所有组件都是向量,无论是逻辑向量、数值向量,还是字符向量(甚至可以是混合向量)。

所有向量的长度都相同。

根据上述规则,我们可以得出这样的推断,即可以将数据帧想象成一个表,该表一般有一定数量的列(以组成这些列的向量来表示)和一定数量的行(行的数量与向量的长度一致),如图1-10所示。虽然要遵守上述两条规则,但是规则中并没有对列的类型做出限制,即数据帧中多个列的类型可以是不同的,例如数据帧中可以存在数值类型和布尔类型的列。

图1-10

可以想象,数据帧是一种非常方便的数据存储方式,特别适用于存储结构化的数据集,如实验观察数据或金融交易数据。数据帧允许在每一行中存储观察数据,并在每一列中存储给定观察数据的属性。在后文中,读者将会更深入地理解数据帧。

虽然数据帧是列表的逻辑子集,但是它具有创建和处理数据帧的整套专用函数。

创建一个数据帧类似于创建一个列表,只不过使用的函数有所不同。同样,创建数据帧的函数被简单地命名为data.frame(),示例如下:

a_data_frame <- data.frame(first_attribute =c("alpha","beta","
gamma"),second_attribute = c(14,20,11))

请注意:对于每一个向量(即每一列),将运算符“=”之前的字符作为列的名称。另外,还需要注意以下两点。

如果不给向量指定名称,那么向量就会被随机地赋予一个难记且非常不友好的名称,在上述示例中,第一个向量会被命名为“c..alpha....beta....gamma..”,而第二个向量会被命名为“c.14..20..11..”。所以,强烈建议读者在创建数据帧时,给向量指定名称。

可以指定包含空格的列名,如将第一个向量的名称改为“first attribute”(取代原来的first_attribute),这时只需要在名称上添加双引号即可,示例如下:

a_data_frame <- data.frame("first attribute" ...)

实际上,并不推荐第二种指定包含空格的列名的做法,因为在后续的代码中再次使用这个向量时,该做法会让事情变得复杂,从而引发很多令人烦恼的后果。

那么,如何选择并展示数据帧的一列呢?答案是使用符号“$”,具体如下:

a_data_frame$second_attribute
[1] 14 20 11

可以用类似的方法向数据帧中添加一列:

a_data_frame$third_attribute <- c(TRUE,FALSE,FALSE)

1.5.5 函数

简单来说,函数是用来操作和处理向量、列表或数据帧的方法。这个定义也许不够严谨,但它突出了一个重点,即函数会接收一些输入参数,这些参数可以是向量(甚至只包含一个元素)、列表或数据帧,然后该函数会输出一个结果,通常也是向量、列表或数据帧。

执行文件系统操作及其他特定任务的函数则是例外,在其他语言中,这些例外通常会被称为过程。例如1.5.1节中遇到过的file.create()函数。

R语言最受赞赏的特性之一就是可以很容易地查看其所有可用函数的定义,只需要使用函数的唯一名称作为命令提交即可,无须输入括号。现在,请读者尝试查看mode()函数的定义,代码如下:

mode
 
function (x)
 {
  if (is.expression(x))
   return("expression")
 if (is.call(x))
   return(switch(deparse(x[[1L]])[1L], `(` = "(", "call"))
 if (is.name(x))
   "name"
else switch(tx <- typeof(x), double = , integer = "numeric",
   closure = , builtin = , special = "function", tx)
}
<bytecode: 0x102264c98>
<environment: namespace:base>

本书不打算深入探讨mode()函数的细节内容,但需要读者注意函数定义的一些结构元素。

这里的function()表示mode()函数本身。

对mode()函数的唯一参数x进行了明确说明。

在function()调用之后的所有内容都用花括号括起来,花括号里面的内容是函数体,它包含对输入参数的所有计算和操作。

在R语言中,上述函数定义的结构元素是定义一个函数所需要的最少元素,可简化表示如下:

function_name <- function(arguments){
    [function body]
}

既然已经知道了函数定义的原理,现在就定义一个简单的函数,即给任意输入的数字加上2:

adding_two <- function(the_number){
the_number + 2
}

这个函数可行吗?当然可行。 为了测试这个函数,必须先执行声明函数定义的两行代码,然后就可以使用读者的自定义函数了:

adding_two( the_number = 4)
[1] 6

继续介绍更加复杂但与函数更相关的概念——函数内的赋值。设想读者正在编写一个函数,并将函数结果存储在一个名为“function_result”的向量中,读者编写的代码可能如下所示:

my_func <- function(x){
function_result <- x / 2
}

假如x的参数值为4,当执行这个函数时,函数的输出应该是一个值为2、名称为“function_result”的对象。

现在请读者根据前面的知识,尝试输出这个对象:

function_result

执行结果为:

Error: object function_result not found

为什么会这样呢?上述错误输出是由函数内赋值的规则所控制的,规则的具体内容如下。

函数内部能够查看并使用函数外部定义的变量。

函数内部定义的变量只能在函数内部使用。

那么,如何将function_result对象从函数内部输出呢?通常有以下两种方式。

使用<<-运算符——通常称之为超赋值运算符。

使用assign()函数。

使用超赋值运算符来重新编写上述函数,具体如下:

my_func <- function(x){
  function_result <<- x / 2
}

如果试着运行它,会发现function_result对象出现在IDE的变量资源管理器中。需要说明的是,从函数内导出对象和将对象作为函数的返回结果是不一样的,如下面的函数所示:

my_func <- function(x){
  function_result <- x / 2
  function_result
}

如果再次尝试运行my_func(4),控制台会输出如下结果:

[1] 2

但在IDE的变量资源管理器中却找不到function_result对象,这是为什么呢?原因是,在上面的函数中,function_result对象的值成了函数的返回值。不管怎样,与上述第一个超赋值运算符的例子类似,这个对象现在是通过标准赋值运算符定义的。