I. 導論
簡單來講,編程是借助計算機來解決某個問題。學習編程的就是訓練我們解決問題的能力。有這樣一種說法:在未來,不會編程的人即是文盲。
1 為什么要學習R編程
大部分情況下解決某些問題還需要依賴一些事實或數據,結合數據分析的框架和計算工具來幫助我們決策和判斷。這時候R語言編程就會派上用場。例如從大的方面來看,投資方要決定在何處建立風力發電場,就需要采集天氣數據加以建模分析,評估各項目方案。從小的方面來看,個人是否應該購買某個理財產品,你需要獲取過去的市場信息,模擬未來可能的變化,計算該項資產未來的期望收益和標准差。所以說學習R編程就是學習在數據環境中解決問題,從中磨練技術、鍛煉智力,還能得到滿足的快感。
2 如何學習R編程
- 讀代碼
- 寫代碼
編程無法在課堂或書本中學到,在游泳池里學游泳是最佳的方法,也是唯一的方法。Learn Python The Hard Way一書的作者Zed A. Shaw曾說過“The Hard Way Is Easier”。所以就算是按照教材重復打一遍代碼,也會有相當的收獲。此外還要按照規范來編寫代碼,養成良好的習慣,包括各種符號的用法和良好的注釋。在注釋里作筆記是也一個好的學習方法,很多時候你只需要將舊代碼略作修改就可以用到其它地方。
3 學習R編程的資源
- 書籍
S Programming
The Art of R Programming
A First Course in Statistical Programming with R
software for data analysis programming with R
Introduction to Scientific Programming and Simulation Using R
- 論壇和博客
http://cos.name/cn/forum/15
http://www.r-bloggers.com/
http://www.statmethods.net/index.html
http://zoonek2.free.fr/UNIX/48_R/all.html
http://www.rdatamining.com/
http://www.r-statistics.com/
http://www.inside-r.org/
http://r-ke.info/
http://wiki.stdout.org/rcookbook/
4 如何獲得幫助
R中的幫助文檔非常有用,其中有四種類型的幫助
- help(functionname) 對已經加載包所含的函數顯示其幫助文檔,用?號也是一樣的。
- help.search('keyword') 對已經安裝的包搜索關鍵詞,用??號功能一樣。
- help(package='packagename') 顯示已經安裝的包的描述和函數說明
- RSiteSearch('keyword') 在官方網站上聯網搜索
5 R語言的啟動
- R語言啟動后會首先查找有無.Rprofile文檔,用戶可通過編輯.Rprofile文檔來自定義R啟動環境,該文件可放在工作目錄或安裝目錄中。
- 之后R會查找在工作目錄有無.RData文檔,若有的話將自動加載恢復之前的工作內容。
- 在R中所有的默認輸入輸出文件都會在工作目錄中。getwd() 報告工作目錄,setwd() 負責設置工作目錄。在win窗口下也可以點擊Change Working Directory來更改。
- Sys.getenv('R_HOME') 會報告R主程序安裝目錄
- ?Startup可以得到更多關於R啟動時的幫助
II. 對象和類
R是一種基於對象(Object)的語言,所以你在R語言中接觸到的每樣東西都是一個對象,一串數值向量是一個對象,一個函數是一個對象,一個圖形也是一個對象。基於對象的編程(OOP)就是在定義類的基礎上,創建與操作對象。
對象中包含了我們需要的數據,同時對象也具有很多屬性(Attribute)。其中一種重要的屬性就是它的類(Class),R語言中最為基本的類包括了數值(numeric)、邏輯(logical)、字符(character)、列表(list),在此基礎上構成了一些復合型的類,包括矩陣(matrix)、數組(array)、因子(factor)、數據框(dataframe)。除了這些內置的類外還有很多其它的,用戶還可以自定義新的類,但所有的類都是建立在這些基本的類之上的。
我們下面來用一個簡單線性回歸的例子來了解一下對象和類的處理。
# 創建兩個數值向量 x <- runif(100) y <- rnorm(100)+5*x # 用線性回歸創建模型,存入對象model model <- lm(y~x)
好了,現在我們手頭上有一個不熟悉的對象model,那么首先來看看它里面藏着什么好東西。最有用的函數命令就是attributes(model),用來提取對象的各種屬性,結果如下:
< attributes(model) $names [1] "coefficients" "residuals" "effects" [4] "rank" "fitted.values" "assign" [7] "qr" "df.residual" "xlevels" [10] "call" "terms" "model" $class [1] "lm"
可以看到這個對象的類是“lm”,這意味着什么呢?我們知道對於不同的類有不同的處理方法,那么對於modle這個對象,就有專門用來處理lm類對象的函數,例如plot.lm()。但如果你用普通的函數plot()也一樣能顯示其圖形,Why?因為plot()這種函數會自動識別對象的類,從而選擇合適的函數來對付它,這種函數就稱為泛型函數(generic function)。你可以用methods(class=lm)來了解有哪些函數可適用於lm對象。
好了,我們已經知道了model的底細了,你還想知道x的信息吧。如果運行attributes(x),會發現返回了空值。這是因為x是一個向量,對於向量這種內置的基本類,attributes是沒有什么好顯示的。此時你可以運行mode(x),可觀察到向量的類是數值型。如果運行mode(model)會有什么反應呢?它會顯示lm類的基本構成是由list組成的。當然要了解對象的類,也可以直接用class(),如果要消除對象的類則可用unclass()。
從上面的結果我們還看到names這個屬性,這如同你到一家餐廳問服務生要一份菜單,輸入names(model)就相當於問model這個對象:Hi,你能提供什么好東西嗎?如果你熟悉回歸理論的話,就可以從names里頭看到它提供了豐富的回歸結果,包括回歸系數(coefficients)、殘差(residuals)等等,調用這些信息可以就象處理普通的數據框一樣使用$符號,例如輸出殘差可以用model$residuals。當然用泛型函數可以達到同樣的效果,如residuals(model),但在個別情況下,這二者結果是有少許差別的。
我們已經知道了attributes的威力了,那么另外一個非常有用的函數是str(),它能以簡潔的方式顯示對象的數據結構及其內容,試試看,非常有用的。
III. 輸入與輸出
如同ATM機一樣,你首先得輸入銀行卡,才能輸出得到鈔票。數據分析也是如此,輸入輸出數據在分析工作中有重要的地位。下面對R語言中一些重要的輸入輸出函數進行小結,而其它的函數請參考官方指南。
1 讀取鍵盤輸入
如果只有很少的數據量,你可以直接用變量賦值輸入數據。若要用交互方式則可以使用readline()函數輸入單個數據,但要注意其默認輸入格為字符型。scan()函數中如果不加參數則也可以用來手動輸入數據。如果加上文件名則是從文件中讀取數據。
2 讀取表格文件
讀取本地表格文件的主要函數是read.table(),其中的file參數設定了文件路徑,注意路徑中斜杠的正確用法(如"C:/data/sample.txt"),header參數設定是否帶有表頭。sep參數設定了列之間的間隔方式。該函數讀取數據后將存為data.frame格式,而且所有的字符將被轉為因子格式,如果你不想這么做需要記得將參數stringsAsFactors設為FALSE。與之類似的函數是read.csv()專門用來讀取csv格式。
如果是想抓去網頁上的某個表格,那么可以使用XML包中的readHTMLTable()函數。例如我們想獲得google統計的訪問最多的1000名網站數據,則可以象下面這樣做。
url <- 'http://www.google.com/adplanner/static/top1000/' data <- readHTMLTable(url) names(data) head(data[[2]])
3 讀取文本文件
有時候需要讀取的數據存放在非結構化的文本文件中,例如電子郵件數據或微博數據。這種情況下只能依靠readLines()函數,將文檔轉為以行為單位存放的list格式。例如我們希望讀取wikipedia的主頁html文件的前十行。
data <- readLines('http://en.wikipedia.org/wiki/Main_Page',n=10)
另外,scan()也有豐富的參數用來讀取非結構化文檔。
4 批量讀取本地文件
在批量讀取文檔時一般先將其存放在某一個目錄下。先用dir()函數獲取目錄中的文件名,然后用paste()將路徑合成,最后用循環或向量化方法處理文檔。例如:
doc.names <- dir("path") doc.path <- sapply(doc.names,function(names) paste(path,names,sep='/')) doc <- sapply(doc.path, function(doc) readLines(doc))
5 寫入文件
write.table()與write.csv()函數可以很方便的寫入表格型數據文檔,而cat()函數除了可以在屏幕上輸出之外,也能夠輸出成文件。
另外若要與MySQL數據庫交換數據,則可以使用RMySLQ包。
IV. 字符串處理
盡管R語言的主要處理對象是數字,而字符串有時候也會在數據分析中占到相當大的份量。特別是在文本數據挖掘日趨重要的背景下,在數據預處理階段你需要熟練的操作字符串對象。當然如果你擅長其它的處理軟件,比如Python,可以讓它來負責前期的臟活。
獲取字符串長度:nchar()能夠獲取字符串的長度,它也支持字符串向量操作。注意它和length()的結果是有區別的。
字符串粘合:paste()負責將若干個字符串相連結,返回成單獨的字符串。其優點在於,就算有的處理對象不是字符型也能自動轉為字符型。
字符串分割:strsplit()負責將字符串按照某種分割形式將其進行划分,它正是paste()的逆操作。
字符串截取:substr()能對給定的字符串對象取出子集,其參數是子集所處的起始和終止位置。
字符串替代:gsub()負責搜索字符串的特定表達式,並用新的內容加以替代。sub()函數是類似的,但只替代第一個發現結果。
字符串匹配:grep()負責搜索給定字符串對象中特定表達式 ,並返回其位置索引。grepl()函數與之類似,但其后面的"l"則意味着返回的將是邏輯值。
一個例子:
我們來看一個處理郵件的例子,目的是從該文本中抽取發件人的地址。該文本在此可以下載到。郵件的全文如下所示:
---------------------------- Return-Path: skip@pobox.com Delivery-Date: Sat Sep 7 05:46:01 2002 From: skip@pobox.com (Skip Montanaro) Date: Fri, 6 Sep 2002 23:46:01 -0500 Subject: [Spambayes] speed Message-ID: <15737.33929.716821.779152@12-248-11-90.client.attbi.com> If the frequency of my laptop's disk chirps are any indication, I'd say hammie is about 3-5x faster than SpamAssassin. Skip ----------------------------
# 用readLines函數從本地文件中讀取郵件全文。 data <- readLines('data') # 判斷對象的類,確定是一個文本型向量,每行文本是向量的一個元素。 class(data) # 從這個文本向量中找到包括有"From:"字符串的那一行 email <- data[grepl('From:',data)] #將其按照空格進行分割,分成一個包括四個元素的字符串向量。 from <- strsplit(email,' ') # 上面的結果是一個list格式,轉成向量格式。 from <- unlist(from) # 最后搜索包含'@'的元素,即為發件人郵件地址。 from <- from[grepl('@',from)]
在字符串的復雜操作中通常會包括正則表達式(Regular Expressions),關於這方面內容可以參考?regex
V. 向量化運算
和matlab一樣,R語言以向量為基本運算對象。也就是說,當輸入的對象為向量時,對其中的每個元素分別進行處理,然后以向量的形式輸出。R語言中基本上所有的數據運算均能允許向量操作。不僅如此,R還包含了許多高效的向量運算函數,這也是它不同於其它軟件的一個顯著特征。向量化運算的好處在於避免使用循環,使代碼更為簡潔、高效和易於理解。本文來對apply族函數作一個簡單的歸納,以便於大家理解其中的區別所在。
所謂apply族函數包括了apply,sapply,lappy,tapply等函數,這些函數在不同的情況下能高效的完成復雜的數據處理任務,但角色定位又有所不同。
apply()函數的處理對象是矩陣或數組,它逐行或逐列的處理數據,其輸出的結果將是一個向量或是矩陣。下面的例子即對一個隨機矩陣求每一行的均值。要注意的是apply與其它函數不同,它並不能明顯改善計算效率,因為它本身內置為循環運算。
m.data <- matrix(rnorm(100),ncol=10) apply(m.data,1,mean)
lappy()的處理對象是向量、列表或其它對象,它將向量中的每個元素作為參數,輸入到處理函數中,最后生成結果的格式為列表。在R中數據框是一種特殊的列表,所以數據框的列也將作為函數的處理對象。下面的例子即對一個數據框按列來計算中位數與標准差。
f.data <- data.frame(x=rnorm(10),y=runif(10)) lapply(f.data,FUN=function(x) list(median=median(x),sd=sd(x))
sapply()可能是使用最為頻繁的向量化函數了,它和lappy()是非常相似的,但其輸出格式則是較為友好的矩陣格式。
sapply(f.data,FUN=function(x)list(median=median(x),sd=sd(x))) class(test)
tapply()的功能則又有不同,它是專門用來處理分組數據的,其參數要比sapply多一個。我們以iris數據集為例,可觀察到Species列中存放了三種花的名稱,我們的目的是要計算三種花瓣萼片寬度的均值。其輸出結果是數組格式。
head(iris) attach(iris) tapply(Sepal.Width,INDEX=Species,FUN=mean)
與tapply功能非常相似的還有aggregate(),其輸出是更為友好的數據框格式。而by()和上面兩個函數是同門師兄弟。
另外還有一個非常有用的函數replicate(),它可以將某個函數重復運行N次,常常用來生成較復雜的隨機數。下面的例子即先建立一個函數,模擬扔兩個骰子的點數之和,然后重復運行10000次。
game <- function() { n <- sample(1:6,2,replace=T) return(sum(n)) } replicate(n=10000,game())
最后一個有趣的函數Vectorize(),它能將一個不能進行向量化運算的函數進行轉化,使之具備向量化運算功能。
VI. 循環與條件
循環
for (n in x) {expr}
R中最基本的是for循環,其中n為循環變量,x通常是一個序列。n在每次循環時從x中順序取值,代入到后面的expr語句中進行運算。下面的例子即是以for循環計算30個Fibonacci數。
x <- c(1,1) for (i in 3:30) { x[i] <- x[i-1]+x[i-2] }
while (condition) {expr}
當不能確定循環次數時,我們需要用while循環語句。在condition條件為真時,執行大括號內的expr語句。下面即是以while循環來計算30個Fibonacci數。
x <- c(1,1) i <- 3 while (i <= 30) { x[i] <- x[i-1]+x[i-2] i <- i +1 }
條件
if (conditon) {expr1} else {expr2}
if語句用來進行條件控制,以執行不同的語句。若condition條件為真,則執行expr1,否則執行expr2。ifesle()函數也能以簡潔的方式構成條件語句。下面的一個簡單的例子是要找出100以內的質數。
x <- 1:100 y <- rep(T,100) for (i in 3:100) { if (all(i%%(2:(i-1))!=0)){ y[i] <- TRUE } else {y[i] <- FALSE } } print(x[y])
在上面例子里,all()函數的作用是判斷一個邏輯序列是否全為真,%%的作用是返回余數。在if/else語句中一個容易出現的錯誤就是else沒有放在}的后面,若你執行下面的示例就會出現錯誤。
logic = 3 x<- c(2,3) if (logic == 2){ y <- x^2 } else { y<-x^3 } show(y)
一個例子
本例來自於"introduction to Scientific Programming and Simulatoin Using R"一書的習題。有這樣一種賭博游戲,賭客首先將兩個骰子隨機拋擲第一次,如果點數和出現7或11,則贏得游戲,游戲結束。如果沒有出現7或11,賭客繼續拋擲,如果點數與第一次扔的點數一樣,則贏得游戲,游戲結束,如果點數為7或11則輸掉游戲,游戲結束。如果出現其它情況,則繼續拋擲,直到贏或者輸。用R編程來計算賭客贏的概率,以決定是否應該參加這個游戲。
craps <- function() { #returns TRUE if you win, FALSE otherwise initial.roll <- sum(sample(1:6,2,replace=T)) if (initial.roll == 7 || initial.roll == 11) return(TRUE) while (TRUE) { current.roll <- sum(sample(1:6,2,replace=T)) if (current.roll == 7 || current.roll == 11) { return(FALSE) } else if (current.roll == initial.roll) { return(TRUE) } } } mean(replicate(10000, craps()))
從最終結果來看,賭客贏的概率為0.46,長期來看只會往外掏錢,顯然不應該參加這個游戲了。最后要說的是,本題也可以用遞歸來做。
VII. 程序查錯
寫程序難免會出錯,有時候一個微小的錯誤需要花很多時間來調試程序來修正它。所以掌握必要的調試方法能避免很多的無用功。
基本的除錯方法是跟蹤重要變量的賦值情況。在循環或條件分支代碼中加入顯示函數能完成這個工作。例如cat('var',var,'\n')。在確認程序運行正常后,可以將這行代碼進行注釋。好的編程風格也能有效的減少出錯的機會。在編寫代碼時先寫出一個功能最為簡單的功能,然后在此基礎上逐步添加其它復雜的功能。對輸出結果進行繪圖或統計匯總也能揭示一些潛在的問題。
另一種避免出錯的方法是盡量使用函數。使用函數能將一個大的程序分解成幾個小型的模塊。一個函數模塊只負責實現某一種功能的實現。這樣容易理解程序,而且容易針對各函數的輸入、計算、輸出分別進行查錯調試。R語言中函數的運行不會影響到全局變量,所以使用函數基本上不會有什么副作用。
但是在使用函數時需要注意的問題是輸入參數的不可預測性。未預料到的輸入參數會產生奇怪的或是錯誤的輸出,所以在函數起始部分就要用條件語句來檢查參數的正確與否。如果輸入參數不正確,可以用下面的語句來停止程序執行stop('your message here.')。
對函數進行調試的重要工具是browser(),它可以使我們進入調試模式逐行運行代碼。在函數中的某一行插入browser()后,在函數執行時會在這一行暫停中斷,並顯示一個提示符。此時我們可以在提示符后輸入任何R語言的交互式命令進行檢查調試。輸入n則會逐行運行程序,並提示下一行將運行的語句。輸入c會直接跳到下一個中斷點。而輸入Q則會直接跟出調試模式。
debug()函數和browser()是相似的,如果你認為某個函數,例如fx(x),有問題的話,使用debug(fx(x))即可進入調試模式。它本質上是在函數的第一行加入了browser,所以其它提示和命令都是相同的。其它與程序調試有關的函數還包括:trace(),setBreakpoint(),traceback(),recover()