借助面向对象的编码风格,并加以合理的抽象,我们可以简单地模仿对象的重要特性,于是,问题和模型之间的转换就变得清晰自然
举例,下面vec1类型是double,意味其内部类型或者说存储模式是双精度浮点型数字。但它的类是numeric。
下面data1类型是list,意味data1的内部类型或者存储模式是列表,但它的S3类是data.frame。
一个类可以用多种方法定义它的行为,尤其是它与其他类的关系。在S3系统中,我们可以创建泛型函数(generic function),对于不同的类,由泛型函数决定调用哪个方法,这就是S3方法分派(method dispatch)的工作机理。
R中有许多基于某个通用目的定义的S3泛型函数,我们先看看head()与tail()。head()展示一个数据对象的前n条记录,tail()展示后n条。这跟x[1:n]是不同的,因为对不同的类的对象,记录的定义是不同的。对原子向量(数值、字符向量等),前n条记录指前n个元素。但对于数据框,前n条记录指前n行而不是前n列。
我们发现函数中并没有实际的操作细节。它调用UseMethod(head)来让泛型函数head()执行方法分派,也就是说,对于不同的类,它可能有不同的执行方式(过程)。
注意,方法都是以method.class形式表示,如果我们输入一个data.frame,head()会调用head.data.frame方法。当没有方法可以匹配对象的类时,函数会自动转向method.default方法。这就是方法分派的一个实际过程。
S3泛型函数和方法在统一各个模型的使用方式上是最有用的。比如我们可以创建一个线性模型,以不同角度查看模型信息:
线性模型本质上是由模型拟合产生的数据字段构成的列表,所以lm1的类型是list,但是它的类是lm,因此泛型函数根据lm选择方法:
甚至没有明确调用S3泛型函数时,S3方法分派也会自动进行。如果我们输入lm1:
为什么打印出来的不像列表呢?因为print()是一个泛型函数,它为lm选择了一个方法来打印线性模型最重要的信息。我们可以调用getS3method(print, lm)获取实际使用的方法与想象的进行验证:
print()展示模型的一个简要版本,summary()展示更详细的信息。summary()也是一个泛型函数,它为模型的所有类提供了许多方法:
实际上,summary()的输出结果也是一个对象,包含的数据都可以被访问。在这个例子里,这个对象是一个列表,是summary.lm类,它有可供print()选择的自己的方法:
还有一些其他有用的且与模型相关的泛型函数,例如plot(),predict()。不同的内置模型和第三方扩展包提供的模型都能实现这些泛型函数。
为避免依次生成这4个图,我们用par()将绘图区域划分为2x2的子区域。
利用predict()我们可以使用模型对新数据进行预测,泛型函数predict()自动选择正确的方法用新数据进行预测:
这个函数既可以用在样本内,又可以用在样本外。如果我们为模型提供新数据,它就进行样本外预测。
这里的fitted()也是泛型函数,等价于lm1$fitted.values,拟合值等于用原始数据得到的预测值,即用原始数据构建的模型预测原始数据,predict(lm1, mtcars)。
真实值与拟合值的差称为残差,可以通过另一个泛型函数residuals()获得。
这些泛型函数不仅适用于lm、glm和其他内置模型,也适用于其他扩展包提供的模型。
我们之所以能够使用相同的方法,是因为这个包的作者希望函数调用的方式与调用R内置函数保持一致。
在定义泛型函数时,我们创建一个函数去调用UseMethod()出发方法分派。然后对泛型函数想要作用的类创建带有method.class形式的方法函数,同时还要创建带有method.default形式的默认方法来应对所有其他情况。
下面我们创建一个新的泛型函数generic_head(),它有两个参数:输入对象x和需要提取的记录条数n。泛型函数仅仅调用UseMethod(generic_head)来让R根据输入对象x的类执行方法分派。