R語言自學系列(3) — 迴圈、流程控制與物件導向設計
前言
程式語言最重要的點莫過於這三者,尤其物件導向的程式設計(Object-oriented programming),這樣的方式非常便於版本管理,實務上是多人專案的基礎,你可以想像得到,當一次複雜的數據分析專案進行時,每個人負責不同的function,當這些function又須要經過他人的前處理時,很容易造成覆蓋原有代碼等悲劇,因此今天就來談談R語言如何實現這些操作。
條件判斷(Conditionals)
大家對於條件判斷一定不陌生,if-else-ifelse是組成R語言條件判斷的三個核心,與Python稍微有些不同(是if-else-elif),我在系列文的第一篇有稍微提到,這邊再貼一次:
可以看到最主要的差別是括號的使用,這一點比較接近C語言的撰寫方式,跟Python完全用縮排有所不同,接著就是條件必須使用括號,而針對條件,R語言除了基本的大於、小於之外,也同樣支援一些語法如:
- 是否位於:x %in% c(4,5,6,7,8)
- 否定表示(用於建立感知器非常強大):!(x>3)
- 交集:c(1,2,3,4,5) & c(2,2,3,5,6)
- 或、聯集:x > 3 | y > 3
另外要注意的是,與Python一樣在比對值的時候要用到的符號是"==",而非"=",否則值會被蓋過去,有了這些基礎,我們就可以應用在迴圈上!
迴圈與流程控制(Loop and Control Flows)
搭配條件判斷,我們可以用以執行複雜的演算法,除了上圖提及的for迴圈以外,還有while與repeat兩種迴圈方式,並透過break與next來控制流程,for 迴圈與 while 迴圈十分類似,不同的是,前者在執行的時候會自動計數,而後者則是單純的條件判斷,寫法分別如下:
顯而易見,在while迴圈中必須新增一個計數器讓x自動加總,整個迴圈是透過TRUE/FALSE的方式來判斷的,因此我們可以用while迴圈輕易地製造一個無窮迴圈來,比方說把條件從 i<10改成 i>0。
repeat迴圈的判斷同樣需要經過條件,不同的是必須在迴圈底下安插if判斷式,這使得repeat的整體複雜度提高,也因此可以配合break與next等方式來控制條件開關。值得一提的是,R語言中有許多方式可以讓使用者模擬變數,舉例來說 rep(5,1) 會連續輸出五個為1的值,這使得我們之後在抽樣方法上多出了更多選擇,同理配合repeat的應用,就可以實現比較複雜的判斷式,也可以做為神經網絡上的節點選擇。
物件導向 - 函數與類別 (Functions and Classes)
上圖是以銀行來比喻類別、屬性與方法間的關係,當然屬性跟方法可以很多種,比方說定義一個屬性叫做名稱叫做土地銀行,或再定義一個方法叫做調整持股人比例等。而物件導向簡而言之,就是將物件看成是程式的基本單位,這個單位可以是變數、列表(實際上這兩個可以是函數或是屬性得更為小單位)、屬性或函數,製造出上下階層的交互關係,一般而言,物件導向的程式設計具有封裝性(Encapsulation)、繼承性(Inheritance)、多型(Polymorphism)與抽象性(Abstraction),但在介紹這四種性質以及它們帶來的好處以前,我們先要了解函數與類別做為高於變數、列表矩陣等物件的方法,在R語言中是怎麼實現的:
R語言中的函式定義與Python非常相近,我分別寫了一個計算樣本自我相關係數的函數,大家可以比較兩種寫法的差異:
從上圖我們可以發現兩者的設定概念十分相似,幾乎只有縮排與括號排序的差異而已,然而,在類別方法中就差異非常大,R語言的物件導向相較於Python一般透過class()與__init__, self等參數來宣告類別,多出了三種不同的類別分別式:S3、S4與Reference Class,在談到這三者之前,我們先看一下Python對於類別的思路是怎麼樣的:
我們可以看到,現在Python能夠透過FinanceTerm這個大類別中透過instance去調出pv_transform這個方法來進行運算,這就是物件導向的好處之一,而比較特別的是,Python在定義物件時的__init__可以說是起手式,這允許你在該類別之下建立不同的屬性,舉例來說,我可以定義一個Finance("年金"),就創建了一個名稱叫做"年金"的物件,同樣地,我也可以定義更多的屬性比方說後面函數使用到的,未來現金流、票面利率等等。
那麼R語言又是怎麼運作的?方才提到,R語言有三種不同的類別方式,分別有不同的使用習慣與應用情境,有興趣的朋友可以參考文章最下方的連結,有非常多人探討過這個問題,在這裡我們探討S4物件與Reference Class兩種不同的方法,前者屬於內建函數的嵌套,後者則更像是單一類別定義。
S4類別有很完整的遞進次序,包括定義類別、建立物件、建立方法最後使用該方法(比如S3就不用定義類別,但我個人更偏好有嚴謹次序的程序建立),以下為簡單的程式碼展示:
S4屬於內嵌函數,另外一種寫法是可以定義完類別以後,透過setGeneric()建立連結窗口來結合方法與類別,這樣的作法雖然繁複,但更加貼近原始語言寫法,而且可以支援一個方法連結多個類別等操作,因此整體的操作還是非常靈活的,我們再來看下一個Reference Class方法:
除了調用屬性的操作符號從"@"(slots)變成"$"(fields)以外,可以看到Reference Class不需要另外拉出方法,雖然少了一些嚴謹性,但有時候程式碼可以變得更加簡潔,對於不需要重複調用某方法到不同類別的某些情況下來說,也是十分方便。
以下的連結是我認為講得非常簡單明瞭的範例說明,裏頭額外提到S3方法,但我因個人使用習慣就不特別拉出來提一次,基本上方法的選用非常自由,可以端看情況來選擇,沒有孰優孰劣的問題。
物件導向 程式設計的四個特性 (4 features of OOP)
- 封裝性(Encapsulation):封裝性其實很好理解,可以想像成外帶打包,把不同的菜餚打包到一個便當盒裡,便當盒就是我們的類別,菜餚是屬性或是方法。我們回到剛才銀行的例子來說,如果我們今天某銀行有三種交易類別分別是:存款、借款與開戶,我們叫他做簡化版商業銀行(類別),這時候我們有個機構A(物件),在沒有封裝以前,我們想讓機構A運轉我們必須一個一個定義他的業務,但透過封裝,我們只要把A打包到簡化版商業銀行這個類別下,他就可以使用所有該類別的方法了!
- 繼承性(Inheritance):一間商業銀行可以有多種業務,舉例來說,它可以有很多個affiliate來支援各地不同的業務並提供專業諮詢,對於程式來說,這些affiliate實際上可以被叫做子類別,去繼承原本的類別,其中可以只繼承幾個方法,比如在美國的分行不提供非當地以外的投資研究報告等等。這個好處是我們不需要另外分隔程式碼,只需要不斷繼承就可以,甚至可以透過with等方法去控制繼承的終始。
- 多型(Polymorphism):不同類別的同一銀行可能支援相同的業務,但業務執行的方式不同,導致的結果也可能不同,這就是Polymorphism。舉例來說,我們可以定義一個類別叫做一般商業銀行,可以另外定義一個類別叫做農業銀行,兩個銀行都支援放款業務,但實際上,農業銀行的放款業務在流程上或是對於借款者的身分檢查上,與一般商行差異頗大。
- 抽象性(Abstraction):實際上這個比較少真實案例(或至少python跟R很難以這樣舉例),他的意思是一個物件的類別並不是完全具體的,而是有變化性的,舉例來說,土地銀行可以是銀行,可以是專業銀行(台灣唯一可以辦理不動產兼農業信用的銀行),但你也可以說它是國營企業。
整體來說,物件導向程式設計的這四個特性其實對於版本管理與簡化程序員工作上至關重要,尤其是在資料建模上,當數據模型的架構非常複雜,需要多人完成一項專案時,物件導向能確保程式不會被任意覆蓋或是修改,也能確保後續開發上都還能使用統一的方式來進行。
總結
當工作變得越來越複雜(比方說Kaggle團隊競賽),物件導向會變得越來越重要,此外你也會開始在意起程序執行時間,如果要做探索性模型適用或是參數調整時,你鐵定不希望每執行一次就是五到十分鐘的等待,這時候精通迴圈與流程控制就是躲不掉的基本功。下一篇是關於資料清洗與探索式資料分析,如果喜歡我的文章還請不吝拍手窩!See you soon!