內容:
- S3
借助面向對象的編碼風格塞淹,并加以合理的抽象剪菱,我們可以簡單地模仿對象的重要特性,于是纹笼,問題和模型之間的轉換就變得清晰自然纹份。
S3對象
S3對象系統(tǒng)是一個簡單且寬松的面向對象系統(tǒng)。每個基本對象的類型都有一個S3類名稱廷痘。比如integer
,numeric
, character
, logical
, list
和data.frame
都屬于S3類蔓涧。
舉例,下面vec1
類型是double
笋额,意味其內部類型或者說存儲模式是雙精度浮點型數(shù)字元暴。但它的類是numeric
。
vec1 = c(1, 2, 3)typeof(vec1)#> [1] "double"class(vec1)#> [1] "numeric"
下面data1
類型是list
兄猩,意味data1
的內部類型或者存儲模式是列表茉盏,但它的S3類是data.frame
。
data1 = data.frame(x = 1:3, y = rnorm(3))typeof(data1)#> [1] "list"class(data1)#> [1] "data.frame"
理解對象的內部類型與S3類區(qū)別是一個重點枢冤。
一個類可以用多種方法定義它的行為鸠姨,尤其是它與其他類的關系。在S3系統(tǒng)中淹真,我們可以創(chuàng)建泛型函數(shù)(generic function)讶迁,對于不同的類,由泛型函數(shù)決定調用哪個方法核蘸,這就是S3方法分派(method dispatch)的工作機理巍糯。
對象的類不同,其方法分派不同客扎,因此祟峦,區(qū)別對象的類十分重要。
R中有許多基于某個通用目的定義的S3泛型函數(shù)徙鱼,我們先看看head()
與tail()
宅楞。head()
展示一個數(shù)據(jù)對象的前n條記錄,tail()
展示后n條。這跟x[1:n]
是不同的咱筛,因為對不同的類的對象搓幌,記錄的定義是不同的。對原子向量(數(shù)值迅箩、字符向量等)溉愁,前n條記錄指前n個元素。但對于數(shù)據(jù)框饲趋,前n條記錄指前n行而不是前n列拐揭。
查看下head
的函數(shù)內部信息:
head#> function (x, ...) #> UseMethod("head")#> <bytecode: 0x0000000018fcb138>#> <environment: namespace:utils>
我們發(fā)現(xiàn)函數(shù)中并沒有實際的操作細節(jié)。它調用UseMethod("head")
來讓泛型函數(shù)head()
執(zhí)行方法分派奕塑,也就是說堂污,對于不同的類,它可能有不同的執(zhí)行方式(過程)龄砰。
num_vec = c(1, 2, 3, 4, 5)data_frame = data.frame(x = 1:5, y = rnorm(5))
調用函數(shù):
head(num_vec, 3)#> [1] 1 2 3head(data_frame, 3)#> x y#> 1 1 0.537#> 2 2 1.072#> 3 3 0.181
我們可以使用methods()
查看head()
函數(shù)可以實現(xiàn)的所有方法:
methods("head")#> [1] head.data.frame* head.default* head.ftable* head.function* #> [5] head.matrix head.table* #> see '?methods' for accessing help and source code
可以看到head
不僅僅適用于向量和數(shù)據(jù)框盟猖。
注意,方法都是以method.class
形式表示换棚,如果我們輸入一個data.frame
式镐,head()
會調用head.data.frame
方法。當沒有方法可以匹配對象的類時固蚤,函數(shù)會自動轉向method.default
方法娘汞。這就是方法分派的一個實際過程。
內置類和方法
S3泛型函數(shù)和方法在統(tǒng)一各個模型的使用方式上是最有用的夕玩。比如我們可以創(chuàng)建一個線性模型你弦,以不同角度查看模型信息:
lm1 = lm(mpg ~ cyl + vs, data = mtcars)
線性模型本質上是由模型擬合產(chǎn)生的數(shù)據(jù)字段構成的列表,所以lm1
的類型是list
燎孟,但是它的類是lm
禽作,因此泛型函數(shù)根據(jù)lm
選擇方法:
typeof(lm1)#> [1] "list"class(lm1)#> [1] "lm"
甚至沒有明確調用S3泛型函數(shù)時,S3方法分派也會自動進行缤弦。如果我們輸入lm1
:
lm1#> #> Call:#> lm(formula = mpg ~ cyl + vs, data = mtcars)#> #> Coefficients:#> (Intercept) cyl vs #> 39.625 -3.091 -0.939
實際上领迈,print()
函數(shù)被默默地調用了:
print(lm1)#> #> Call:#> lm(formula = mpg ~ cyl + vs, data = mtcars)#> #> Coefficients:#> (Intercept) cyl vs #> 39.625 -3.091 -0.939
為什么打印出來的不像列表呢?因為print()
是一個泛型函數(shù)碍沐,它為lm
選擇了一個方法來打印線性模型最重要的信息。我們可以調用getS3method("print", "lm")
獲取實際使用的方法與想象的進行驗證:
identical(getS3method("print", "lm"), stats:::print.lm)#> [1] TRUE
print()
展示模型的一個簡要版本衷蜓,summary()
展示更詳細的信息累提。summary()
也是一個泛型函數(shù),它為模型的所有類提供了許多方法:
summary(lm1)#> #> Call:#> lm(formula = mpg ~ cyl + vs, data = mtcars)#> #> Residuals:#> Min 1Q Median 3Q Max #> -4.923 -1.953 -0.081 1.319 7.577 #> #> Coefficients:#> Estimate Std. Error t value Pr(>|t|) #> (Intercept) 39.625 4.225 9.38 2.8e-10 ***#> cyl -3.091 0.558 -5.54 5.7e-06 ***#> vs -0.939 1.978 -0.47 0.64 #> ---#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1#> #> Residual standard error: 3.25 on 29 degrees of freedom#> Multiple R-squared: 0.728, Adjusted R-squared: 0.71 #> F-statistic: 38.9 on 2 and 29 DF, p-value: 6.23e-09
實際上磁浇,summary()
的輸出結果也是一個對象斋陪,包含的數(shù)據(jù)都可以被訪問。在這個例子里,這個對象是一個列表无虚,是summary.lm
類缔赠,它有可供print()
選擇的自己的方法:
lm1summary = summary(lm1)typeof(lm1summary)#> [1] "list"class(lm1summary)#> [1] "summary.lm"
查看列表成分:
names(lm1summary)#> [1] "call" "terms" "residuals" "coefficients" #> [5] "aliased" "sigma" "df" "r.squared" #> [9] "adj.r.squared" "fstatistic" "cov.unscaled"
還有一些其他有用的且與模型相關的泛型函數(shù),例如plot()
,predict()
友题。不同的內置模型和第三方擴展包提供的模型都能實現(xiàn)這些泛型函數(shù)嗤堰。
舉例,我們可以對線性模型調用plot()
函數(shù):
oldpar = par(mfrow = c(2, 2))plot(lm1)
par(oldpar)
為避免依次生成這4個圖度宦,我們用par()
將繪圖區(qū)域劃分為2x2的子區(qū)域踢匣。
利用predict()
我們可以使用模型對新數(shù)據(jù)進行預測,泛型函數(shù)predict()
自動選擇正確的方法用新數(shù)據(jù)進行預測:
predict(lm1, data.frame(cyl = c(6, 8), vs = c(1, 1)))#> 1 2 #> 20.1 14.0
這個函數(shù)既可以用在樣本內戈抄,又可以用在樣本外离唬。如果我們?yōu)槟P吞峁┬聰?shù)據(jù),它就進行樣本外預測划鸽。
下面我們創(chuàng)建一幅真實值和擬合值的散點圖输莺,看一看線性模型的預測效果:
plot(mtcars$mpg, fitted(lm1))
這里的fitted()
也是泛型函數(shù),等價于lm1$fitted.values
裸诽,擬合值等于用原始數(shù)據(jù)得到的預測值模闲,即用原始數(shù)據(jù)構建的模型預測原始數(shù)據(jù),predict(lm1, mtcars)
崭捍。
真實值與擬合值的差稱為殘差尸折,可以通過另一個泛型函數(shù)residuals()
獲得。
plot(density(residuals(lm1)), main = "Density of lm1 residuals")
這些泛型函數(shù)不僅適用于lm
殷蛇、glm
和其他內置模型实夹,也適用于其他擴展包提供的模型。
例如我們使用rpart
包粒梦,使用前面的數(shù)據(jù)和公式擬合一個回歸樹模型亮航。
if(!require("rpart")) install.packages("rpart")#> 載入需要的程輯包:rpartlibrary(rpart)
tree_model = rpart(mpg ~ cyl + vs, data = mtcars)
我們之所以能夠使用相同的方法,是因為這個包的作者希望函數(shù)調用的方式與調用R內置函數(shù)保持一致匀们。
typeof(tree_model)#> [1] "list"class(tree_model)#> [1] "rpart"
打印模型:
print(tree_model)#> n= 32 #> #> node), split, n, deviance, yval#> * denotes terminal node#> #> 1) root 32 1130.0 20.1 #> 2) cyl>=5 21 198.0 16.6 #> 4) cyl>=7 14 85.2 15.1 *#> 5) cyl< 7 7 12.7 19.7 *#> 3) cyl< 5 11 203.0 26.7 *
更詳細信息:
summary(tree_model)#> Call:#> rpart(formula = mpg ~ cyl + vs, data = mtcars)#> n= 32 #> #> CP nsplit rel error xerror xstd#> 1 0.6431 0 1.000 1.089 0.2579#> 2 0.0893 1 0.357 0.432 0.0811#> 3 0.0100 2 0.268 0.427 0.0818#> #> Variable importance#> cyl vs #> 65 35 #> #> Node number 1: 32 observations, complexity param=0.643#> mean=20.1, MSE=35.2 #> left son=2 (21 obs) right son=3 (11 obs)#> Primary splits:#> cyl < 5 to the right, improve=0.643, (0 missing)#> vs < 0.5 to the left, improve=0.441, (0 missing)#> Surrogate splits:#> vs < 0.5 to the left, agree=0.844, adj=0.545, (0 split)#> #> Node number 2: 21 observations, complexity param=0.0893#> mean=16.6, MSE=9.45 #> left son=4 (14 obs) right son=5 (7 obs)#> Primary splits:#> cyl < 7 to the right, improve=0.507, (0 missing)#> Surrogate splits:#> vs < 0.5 to the left, agree=0.857, adj=0.571, (0 split)#> #> Node number 3: 11 observations#> mean=26.7, MSE=18.5 #> #> Node number 4: 14 observations#> mean=15.1, MSE=6.09 #> #> Node number 5: 7 observations#> mean=19.7, MSE=1.81
下面對結果進行可視化缴淋,得到樹圖:
oldpar = par(xpd = NA)plot(tree_model)text(tree_model, use.n = TRUE)
par(oldpar)
為現(xiàn)有類定義泛型函數(shù)
在定義泛型函數(shù)時,我們創(chuàng)建一個函數(shù)去調用UseMethod()出發(fā)方法分派泄朴。然后對泛型函數(shù)想要作用的類創(chuàng)建帶有method.class形式的方法函數(shù)重抖,同時還要創(chuàng)建帶有method.default形式的默認方法來應對所有其他情況。
下面我們創(chuàng)建一個新的泛型函數(shù)generic_head()
祖灰,它有兩個參數(shù):輸入對象x和需要提取的記錄條數(shù)n钟沛。泛型函數(shù)僅僅調用UseMethod("generic_head")
來讓R根據(jù)輸入對象x
的類執(zhí)行方法分派。
generic_head = function(x, n) UseMethod("generic_head")
對原子向量提取前n
個元素局扶,因此分別定義generic_head.numeric
恨统、generic_head.character
等叁扫,另外最好定義一個默認方法捕獲不能匹配的其他所有情況:
generic_head.default = function(x, n){ x[1:n]}
現(xiàn)在generic_head
只有一種方法,等于沒有使用泛型函數(shù):
generic_head(num_vec, 3)#> [1] 1 2 3
現(xiàn)在我們還沒有定義針對data.frame
類的方法畜埋,所以當我們輸入數(shù)據(jù)框時莫绣,函數(shù)會自動轉向generic_head.default
,又因為提取的數(shù)量超出列數(shù)悠鞍,所以下面的運行會報錯:
generic_head(data_frame, 3)#> Error in `[.data.frame`(x, 1:n): 選擇了未定義的列
下面為data.frame
定義方法:
generic_head.data.frame = function(x, n) { x[1:n, ]}
現(xiàn)在函數(shù)就可以正常運行了:
generic_head(data_frame, 3)#> x y#> 1 1 0.537#> 2 2 1.072#> 3 3 0.181
因為沒有對參數(shù)進行檢查对室,所以S3類執(zhí)行的方法并不穩(wěn)健。
定義新類并創(chuàng)建對象
現(xiàn)在我們來嘗試構建新類狞玛,class(x)
獲取x
的類软驰,而class(x) = some_class
將x
的類設為some_class
稠集。
使用列表作為底層數(shù)據(jù)結構
列表可能是創(chuàng)建新類時使用最廣泛的數(shù)據(jù)結構辫红,類描述了對象的類型和對象交互作用的方法衍菱,其中對象用于存儲多種多樣几苍、長度不一的數(shù)據(jù)协怒。
下面我們定義一個叫product
的函數(shù)力细,創(chuàng)建一個由name
宜雀、price
和inventory
構成的列表本橙,該列表的類是product
固该。我們還將自己定義它的print
方法锅减。
productor = function(name, price, inventory){ obj = list(name = name, price = price, inventory = inventory) class(obj) = "product" obj}
上面我們創(chuàng)建了一個列表,然后將它的類替換為product
伐坏。我們還可以使用structure()
:
product = function(name, price, inventory){ structure(list(name = name, price = price, inventory = inventory), class = "product")}
現(xiàn)在我們調用product()
函數(shù)生成product
類的實例:
laptop = product("Laptop", 499, 300)
查看它的結構和S3類方法分派:
typeof(laptop)#> [1] "list"class(laptop)#> [1] "product"
此時我們還沒有為該類定義任何方法怔匣,如果print
將按默認列表輸出:
print(laptop)#> $name#> [1] "Laptop"#> #> $price#> [1] 499#> #> $inventory#> [1] 300#> #> attr(,"class")#> [1] "product"
下面我們自定義一個print
方法,使得輸出更緊湊:
print.product = function(x, ...){ cat("<product>\n") cat("name:", x$name, "\n") cat("price:", x$price, "\n") cat("inventory:", x$inventory, "\n") invisible(x)}
print
方法返回輸入對象本身以備后用桦沉,這是一項約定每瞒。
現(xiàn)在我們再來看看輸出:
laptop#> <product>#> name: Laptop #> price: 499 #> inventory: 300
我們可以像操作列表一樣訪問laptop
的成分:
laptop$name#> [1] "Laptop"laptop$price#> [1] 499laptop$inventory#> [1] 300
如果我們創(chuàng)建另一個對象,并將兩者放入一個列表然后打印纯露,print.product
仍然會被調用:
cellphone = product("Phone", 249, 12000)products = list(laptop, cellphone)products#> [[1]]#> <product>#> name: Laptop #> price: 499 #> inventory: 300 #> #> [[2]]#> <product>#> name: Phone #> price: 249 #> inventory: 12000
當products
以列表形式被打印時剿骨,會對每個元素調用print()
泛型函數(shù),再由泛型函數(shù)執(zhí)行方法分派埠褪。
大多數(shù)其他編程語言都對類有正式的定義浓利,而S3沒有,所以創(chuàng)建一個S3對象比較簡單钞速,但我們需要對輸入?yún)?shù)進行充分的檢查贷掖,以確保創(chuàng)建的對象與所屬類內部一致。
除了定義新類玉工,我們還可以定義新的泛型函數(shù)羽资。下面創(chuàng)建一個叫value
的泛型函數(shù),它通過測量產(chǎn)品的庫存值來為product
調用實施方法:
value = function(x, ...) UseMethod("value")value.default = function(x, ...){ stop("Value is undefined")}value.product = function(x, ...){ x$price * x$inventory}
針對其他類遵班,value
調用default
方法并終止運行屠升。
value(laptop)#> [1] 149700value(cellphone)#> [1] 2988000value(data_frame)#> Error in value.default(data_frame): Value is undefined
使用原子向量作為底層數(shù)據(jù)結構
上面我們已經(jīng)演示了創(chuàng)建S3類和泛型函數(shù)的過程,有時候我們需要使用原子向量創(chuàng)建新類狭郑,下面展示百分比形式向量創(chuàng)建過程腹暖。
首先定義一個percent
函數(shù),它檢查輸入是否是數(shù)值向量并將輸入對象類型改為percent
翰萨,percent
類繼承numeric
類:
percent = function(x){ stopifnot(is.numeric(x)) class(x) = c("percent", "numeric") x}
這里的繼承指方法分派首先在percent類中方法找脏答,找不到就去numeric類方法中找。尋找的順序由類名稱的順序決定亩鬼。
pct = percent(c(0.1, 0.05, 0.25))pct#> [1] 0.10 0.05 0.25#> attr(,"class")#> [1] "percent" "numeric"
現(xiàn)在定義方法殖告,讓percent
類以百分比形式存在:
as.character.percent = function(x, ...){ paste0(as.numeric(x) * 100, "%")}
現(xiàn)在我們可以得到字符型了:
as.character(pct)#> [1] "10%" "5%" "25%"
也可以直接調用as.character()
為percent
提供一個format
方法:
format.percent = function(x, ...){ as.character(x, ...)}
format
現(xiàn)在有相同的效果:
format(pct)#> [1] "10%" "5%" "25%"
類似地,我們調用format.percent()
為percent
提供print
方法:
print.percent = function(x, ...){ print(format.percent(x), quote = FALSE)}
這里指定quote=FALSE
使得打印的格式化字符串更像數(shù)字而非字符串雳锋。
pct#> [1] 10% 5% 25%
注意黄绩,使用算術運算符操作后會自動保持輸出向量類不變:
pct + 0.2#> [1] 30% 25% 45%pct * 0.5#> [1] 5% 2.5% 12.5%
可惜使用其他函數(shù)可能不會保持輸入對象的類,比如sum()
玷过、mean()
等:
sum(pct)#> [1] 0.4mean(pct)#> [1] 0.133max(pct)#> [1] 0.25min(pct)#> [1] 0.05
為了確保百分比形式保存爽丹,我們對percent
類實施一些操作:
sum.percent = function(...){ percent(NextMethod("sum"))}mean.percent = function(x, ...){ percent(NextMethod("mean"))}max.percent = function(...){ percent(NextMethod("max"))}min.percent = function(...){ percent(NextMethod("max"))}
NextMethod("sum")
對numeric
類調用sum()
函數(shù),然后再調用percent()
函數(shù)將輸出的數(shù)值向量包裝為百分比形式:
sum(pct)#> [1] 40%mean(pct)#> [1] 13.3333333333333%max(pct)#> [1] 25%min(pct)#> [1] 5%
但如果我們組合一個百分比向量和其他數(shù)值型的值辛蚊,percent
類又會消失掉粤蝎,我們進行相同的改進:
c.percent = function(x, ...){ percent(NextMethod("c"))}
c(pct, 0.12)#> [1] 10% 5% 25% 12%
dan….我們取子集又會有問題
pct[1:3]#> [1] 0.10 0.05 0.25pct[[2]]#> [1] 0.05
同樣地,我們對[
和[[
函數(shù)進行改造:
`[.percent` = function(x, i) { percent(NextMethod('['))}`[[.percent` = function(x, i){ percent(NextMethod("[["))}
此時顯示就正常了:
pct[1:3]#> [1] 10% 5% 25%pct[[2]]#> [1] 5%
實現(xiàn)這些方法后袋马,我們可以在數(shù)據(jù)框中使用:
data.frame(id = 1:3, pct)#> id pct#> 1 1 10%#> 2 2 5%#> 3 3 25%
S3繼承
假設我們想要對一些交通工具初澎,例如汽車、公共汽車和飛機進行建模虑凛。這些交通工具有一些共性碑宴,它們都有名稱、速度卧檐、位置墓懂,而且都可以移動。為了形象化描述它們霉囚,我們定義一個基本類捕仔,稱為vehichle
,用于存儲公共部分盈罐,另外定義car
榜跌、bus
和airplane
這3個子類,它們繼承vehichle
盅粪,但具有自定義的行為钓葫。
首先,定義一個函數(shù)來創(chuàng)建vehicle
對象票顾,它本質上是一個環(huán)境础浮。我們選擇環(huán)境而不是列表帆调,因為需要用到環(huán)境的引用語義,也就是說豆同,我們傳遞一個對象番刊,然后原地修改它,而不會創(chuàng)建這個對象的副本影锈。因此無論什么位置將對象傳遞給函數(shù)芹务,對象總是指向同一個交通工具。
Vehicle = function(class, name, speed) { obj = new.env(parent = emptyenv()) obj$name = name obj$speed = speed obj$position = c(0, 0, 0) class(obj) = c(class, "vehicle") obj}
這里的class(obj) = c(class, "vehicle")
似乎有點語義不明鸭廷。但前者是基礎函數(shù)枣抱,后者是輸入?yún)?shù),R能夠判斷好辆床。
下面函數(shù)創(chuàng)建繼承vehicle
的car
佳晶、bus
和airplane
的特定函數(shù):
Car = function(...){ Vehicle(class = "car", ...)}Bus = function(...){ Vehicle(class = "bus", ...)}Airplane = function(...){ Vehicle(class = "airplane", ...)}
現(xiàn)在我們可以為每一個子類創(chuàng)建實例:
car = Car("Model-A", 80)bus = Bus("Medium-Bus", 40)airplane = Airplane("Big-Plane", 800)
下面為vehicle
提供通用的print
方法:
print.vehicle = function(x, ...){ cat(sprintf("<vehicle: %s>\n", class(x)[1])) cat("name:", x$name, "\n") cat("speed:", x$speed, "km/h\n") cat("position:", paste(x$position, collapse = ", "), "\n")}
因為我們定義的3個子類都有了繼承,所以print
方法通用:
car#> <vehicle: car>#> name: Model-A #> speed: 80 km/h#> position: 0, 0, 0bus#> <vehicle: bus>#> name: Medium-Bus #> speed: 40 km/h#> position: 0, 0, 0airplane#> <vehicle: airplane>#> name: Big-Plane #> speed: 800 km/h#> position: 0, 0, 0
因為交通工具可以移動佛吓,我們創(chuàng)建一個泛型函數(shù)move
來表征這樣的狀態(tài):
move = function(vehicle, x, y, z) { UseMethod("move")}move.vehicle = function(vehicle, movement) { if (length(movement) != 3){ stop("All three dimensions must be specified to move a vehicle") } vehicle$position = vehicle$position + movement vehicle}
這里我們將汽車和公共汽車的移動限定在二維平面上宵晚。
move.bus = move.car = function(vehicle, movement) { if (length(movement) != 2){ stop("This vehicle only supports 2d movement") } movement = c(movement, 0) NextMethod("move")}
這里我們將movement
的第3個緯度強制轉換為0,然后調用NextMethod("move")
來調用move.vehicle()
维雇。
飛機既可以在2維也可以在3維:
move.airplane = function(vehicle, movement) { if (length(movement) == 2){ movement = c(movement, 0) } NextMethod("move")}
下載3種方法都實現(xiàn)了淤刃,進行測試。
move(car, c(1, 2, 3))#> Error in move.car(car, c(1, 2, 3)): This vehicle only supports 2d movement
只能輸入二維吱型,所以提示報錯了逸贾。
move(car, c(1, 2))#> <vehicle: car>#> name: Model-A #> speed: 80 km/h#> position: 1, 2, 0
move(airplane, c(1, 2))#> <vehicle: airplane>#> name: Big-Plane #> speed: 800 km/h#> position: 1, 2, 0
飛機,3維:
move(airplane, c(20,100,50))#> <vehicle: airplane>#> name: Big-Plane #> speed: 800 km/h#> position: 21, 102, 50
注意津滞,airplane的位置是累積的铝侵。因為前面說過,它本質是一個環(huán)境触徐,因此修改move.vehicle()
中的position
不會創(chuàng)建一個副本再修改咪鲜,而是本地修改!
學習自《R語言編程指南》
內容太多撞鹉,下次學習接下來的內容疟丙。
文章作者 王詩翔
上次更新 2018-08-15
許可協(xié)議 CC BY-NC-ND 4.0