R语言入门与实践
上QQ阅读APP看书,第一时间看更新

1.3 函数

要想实现类似随机抽样这样较为复杂的任务,可以用R自带的一些函数。比如,round函数可以实现数字的四舍五入操作,factorial函数可以实现阶乘操作。R中函数的使用方法非常简单,只需把函数的名称敲出来并在其后的括号中键入相应的数据即可。

        round(3.1415)
        ## 3

        factorial(3)
        ## 6

传递到函数中的数据称为该函数的参数(argument)。参数可以是原始数据、R对象,甚至是另一个R函数的返回结果。在最后一种情况下,R函数的执行方式是从内到外,如图1-5所示。

图1-5:在R中使用函数嵌套时,R将从最内层的运算开始解析,直到最外层的运算为止。在这个例子中,R首先找到die这个对象,然后计算所有6个数值的平均值,再进行四舍五入

        mean(1:6)
        ## 3.5

        mean(die)
        ## 3.5

        round(mean(die))
        ## 4

好在有一个R函数可以帮助我们完成“掷骰子”的任务。这个函数便是sample函数,它可以模拟掷骰子。sample函数有两个参数:一个名为x的向量和一个名为size的数字。sample函数的作用便是从向量x中抽取size个元素并返回。

        sample(x = 1:4, size = 2)
        ## 3 2

要想掷骰子并得到一个点数,可将x设置为die,然后从die中抽取一个元素。每次掷都会得到一个新的(可能不同的)点数。

        sample(x = die, size = 1)
        ## 2

        sample(x = die, size = 1)
        ## 1

        sample(x = die, size = 1)
        ## 6

R中的许多函数都包含多个参数以帮助其完成任务。可以给一个R函数设置多个参数,只要用逗号将它们隔开。

你可能已经注意到了,在上面的代码示例中,我用等号将die和1分别与sample函数中的参数名x和size连了起来。你可以设置应该将哪个数据对象赋值给该函数的哪个参数,方法是将这个数据对象的名称与参数名用等号连起来。在给有着多个参数的函数设置参数值的时候,这一点尤为重要;将数据对象明确指定给某个参数名可以避免错误传递数据。然而,对于R函数来说,并不一定要明确指定参数名。你会注意到,R用户大都不会指定R函数的第一个参数的名称。因此上面的代码很可能会写成下面的形式。

        sample(die, size = 1)
        ## 2

通常来说,R函数的第一个参数的名称都比较简单,乍看很难知道它代表什么意思,但是第一个数据对象的含义往往是显而易见的。

那么你如何知道应该调用哪个参数呢?如果你使用了一个该函数不能识别的参数名,那么很可能会得到一条错误的输出信息。

        round(3.1415, corners = 2)
        ## Error in round(3.1415, corners = 2) : unused argument(s) (corners = 2)

对于一个函数,如果你不确定应该如何设置其中的参数,可以通过args函数查看这个函数的所有参数名。具体来说,只要将函数的名称放在args函数的括号中即可。从下面的例子可以看出,round函数有两个参数,一个参数名是x,另一个是digits。

        args(round)
        ## function (x, digits = 0)
        ## NULL

不知道大家注意到没有,args函数显示round的digits参数已被设置为0。因为它本身有一个默认值,所以是可选参数。R函数中经常会有digits这样的可选参数。只要需要,你可以将一个不一样的值赋给一个可选参数。如果不明确赋值,该可选参数就会使用其默认值。比如说,round函数会默认将数值四舍五入到小数点后的0位。要替代该默认值,可以为digits提供不同的值,如下所示。

        round(3.1415, digits = 2)
        ## 3.14

在调用一个包含多个参数的函数时,从第二个参数或者第三个参数开始,应该写出每个参数的名称。为什么呢?首先,这有助于你自己或者其他的代码阅读者理解你的代码。通常来说,第一个参数的含义指向是十分明确的(有时候第二个参数也如此)。但是,要记住每一个R函数的第三个或者第四个参数代表什么,是一件十分困难的事情。其次,更为重要的一点是,详细地写出参数名称可以防止出现错误。

如果你没有写出参数名称,那么R会按顺序将你的值与函数中的参数匹配。例如,在下面的代码中,第一个值是die,该值会与sample函数的第一个参数x匹配。第二个值是1,该值会与第二个参数size匹配。

        sample(die, 1)
        ## 2

如果一个函数包含多个参数,那么你所使用的顺序很可能与R的顺序不一致。这可能会导致值被传递给错误的参数。而详细地写出参数名称可以防止这样的情况发生。R会始终将某个值与其参数名称相匹配,而无论其参数顺序如何。

        sample(size = 1, x = die)
        ## 2

1.4 可放回抽样

在上面的sample函数中,如果设定size = 2,就几乎可以模拟一对骰子了。在运行代码之前,花一分钟想想为什么会是这样。sample函数会返回两个数字,分别对应每个骰子的点数。

        sample(die, size = 2)
        ## 3 4

我刚才之所以说“几乎”是因为这个方法的输出结果有可疑之处。如果我们多次运行这样的模拟,会发现第二个点数永远不同于第一个点数,也就是说,你永远不可能掷出两个三点或者两个一点。两个骰子的点数不可能一模一样。这到底是为什么呢?

原来,sample函数在抽样时默认使用了不可放回抽样(without replacement)。要理解这是什么意思,试想一下sample函数将die数据对象中所有可能的点数都放在了一个罐子中,然后将这些点数一个一个地拿出来,从而排出最终的点数样本。一旦某个点数被取出来,sample函数就把它放在一边,不会重新放进罐中,因此在接下来的抽样中不可能再取到该点数。因此,如果sample函数第一次取的点数为6,那么它第二次就不会再取到同一个点数,因为点数6已经被从罐中拿了出来。虽然sample函数的取样是计算机执行的,但是它遵循了这种物理规律。

这种抽样方法的一个副作用就是每一次取样的结果都与前一次取样的结果息息相关。然而,在现实世界里,当我们掷骰子时,每个骰子之间都是相互独立的。如果第一个骰子的点数是6,这并不妨碍第二个骰子的点数也是6。也就是说,第一次掷骰子的结果不应该对第二次的结果有任何影响。这样的取样逻辑也可以用sample函数实现,只不过需要额外设定参数replace = TRUE。

        sample(die, size = 2, replace = TRUE)
        ## 5 5

参数replace = TRUE的作用就是将sample函数的抽样类型设定为可放回抽样(with replacement)。之前将骰子点数放入罐中的例子可以很好地解释可放回抽样与不可放回抽样的区别。进行可放回抽样时,sample函数从罐中取出一个点数并记录下该点数的值,然后将该点数放回罐中。也就是说,sample函数在每次点数取样之后都将该点数放回(replace)到原罐中。这样的放回操作使得两次掷骰子出现相同的点数成为可能。因为6个点数在每次抽样中都有可能被取到,所以每一次抽样都跟第一次抽样没有区别。

可放回抽样法是创建独立随机样本(independent random sample)的一种简单方法。可以将样本中的每一个值视为一个样本量为1的独立样本,并且它与样本中的其他数值是相互独立的。这才是模拟掷一对骰子的正确方法。

        sample(die, size = 2, replace = TRUE)
        ## 2 4

恭喜你,你已经用R尝试运行了第一个模拟。现在,你已经掌握了模拟掷一对骰子的实现方法。如果你想知道两个骰子的总点数,只需要将模拟的结果直接交给sum函数即可。

        dice <- sample(die, size = 2, replace = TRUE)
        dice
        ## 2 4

        sum(dice)
        ## 6

在dice生成之后,如果我们反复调用它会有什么样的效果呢?R会在每次调用时都生成一对新的点数吗?我们不妨试一下。

          dice
          ## 2 4

          dice
          ## 2 4

          dice
          ## 2 4

显然R并不会每次都生成一对新的点数。每次调用dice, R返回的都是同样的数值,也就是之前用sample函数抽样然后把抽样的点数赋给dice的那一对数值。R并不会重新运行一次sample(die, 2, replace = TRUE)以生成一对新的点数。这其实是有一定益处的。每当将一组结果保存到一个R对象中之后,这些结果就不会改变了。如果对象的值在每次调用时都会发生改变,编程工作将变成一场噩梦。

然而,若有一个对象在每次调用时都重新掷一次骰子,可能会很方便。要实现这样的目的,你需要编写一个自定义的R函数。