I. 導論
簡單來講,編程是藉助計算機來解決某個問題。學習編程的就是訓練我們解決問題的能力。有這樣一種說法:在未來,不會編程的人即是文盲。
大部分情況下解決某些問題還需要依賴一些事實或數據,結合數據分析的框架和計算工具來幫助我們決策和判斷。這時候R語言編程就會派上用場。例如從大的方面來看,投資方要決定在何處建立風力發電場,就需要採集天氣數據加以建模分析,評估各項目方案。從小的方面來看,個人是否應該購買某個理財產品,你需要獲取過去的市場信息,模擬未來可能的變化,計算該項資產未來的期望收益和標準差。所以說學習R編程就是學習在數據環境中解決問題,從中磨練技術、鍛鍊智力,還能得到滿足的快感。
- 讀代碼
- 寫代碼
編程無法在課堂或書本中學到,在游泳池裏學游泳是最佳的方法,也是唯一的方法。Learn
Python The Hard Way一書的作者Zed A. Shaw曾說過“The Hard Way Is Easier”。所以就算是按照教材重複打一遍代碼,也會有相當的收穫。此外還要按照規範來編寫代碼,養成良好的習慣,包括各種符號的用法和良好的註釋。在註釋裏作筆記是也一個好的學習方法,很多時候你只需要將舊代碼略作修改就可以用到其它地方。
- 書籍
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)。除了這些內置的類外還有很多其它的,用戶還可以自定義新的類,但所有的類都是建立在這些基本的類之上的。
我們下面來用一個簡單線性迴歸的例子來了解一下對象和類的處理。
1 |
#
創建兩個數值向量 |
2 |
x
<- runif (100) |
3 |
y
<- rnorm (100)+5*x |
4 |
#
用線性迴歸創建模型,存入對象model |
5 |
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名網站數據,則可以象下面這樣做。
2 |
data
<- readHTMLTable (url) |
3 |
names (data) |
4 |
head (data[[2]]) |
3 讀取文本文件
有時候需要讀取的數據存放在非結構化的文本文件中,例如電子郵件數據或微博數據。這種情況下只能依靠readLines()函數,將文檔轉爲以行爲單位存放的list格式。例如我們希望讀取wikipedia的主頁html文件的前十行。
1 |
data
<- readLines ( 'http://en.wikipedia.org/wiki/Main_Page' ,n=10) |
另外,scan()也有豐富的參數用來讀取非結構化文檔。
4 批量讀取本地文件
在批量讀取文檔時一般先將其存放在某一個目錄下。先用dir()函數獲取目錄中的文件名,然後用paste()將路徑合成,最後用循環或向量化方法處理文檔。例如:
1 |
doc.names
<- dir ( "path" ) |
2 |
doc.path
<- sapply (doc.names, function (names) paste (path,names,sep= '/' )) |
3 |
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: [email protected] Delivery-Date: Sat Sep 7 05:46:01 2002 From: [email protected] (Skip Montanaro) Date: Fri, 6 Sep 2002 23:46:01 -0500 Subject: [Spambayes] speed Message-ID: <[email protected]> If the frequency of my laptop's disk chirps are any indication, I'd say hammie is about 3-5x faster than SpamAssassin. Skip ----------------------------
01 |
#
用readLines函數從本地文件中讀取郵件全文。 |
02 |
data
<- readLines ( 'data' ) |
03 |
#
判斷對象的類,確定是一個文本型向量,每行文本是向量的一個元素。 |
04 |
class (data) |
05 |
#
從這個文本向量中找到包括有"From:"字符串的那一行 |
06 |
email
<- data[ grepl ( 'From:' ,data)] |
07 |
#將其按照空格進行分割,分成一個包括四個元素的字符串向量。 |
08 |
from
<- strsplit (email, '
' ) |
09 |
#
上面的結果是一個list格式,轉成向量格式。 |
10 |
from
<- unlist (from) |
11 |
#
最後搜索包含'@'的元素,即爲發件人郵件地址。 |
12 |
from
<- from[ grepl ( '@' ,from)] |
在字符串的複雜操作中通常會包括正則表達式(Regular Expressions),關於這方面內容可以參考?regex
V. 向量化運算
和matlab一樣,R語言以向量爲基本運算對象。也就是說,當輸入的對象爲向量時,對其中的每個元素分別進行處理,然後以向量的形式輸出。R語言中基本上所有的數據運算均能允許向量操作。不僅如此,R還包含了許多高效的向量運算函數,這也是它不同於其它軟件的一個顯著特徵。向量化運算的好處在於避免使用循環,使代碼更爲簡潔、高效和易於理解。本文來對apply族函數作一個簡單的歸納,以便於大家理解其中的區別所在。
所謂apply族函數包括了apply,sapply,lappy,tapply等函數,這些函數在不同的情況下能高效的完成複雜的數據處理任務,但角色定位又有所不同。
apply()函數的處理對象是矩陣或數組,它逐行或逐列的處理數據,其輸出的結果將是一個向量或是矩陣。下面的例子即對一個隨機矩陣求每一行的均值。要注意的是apply與其它函數不同,它並不能明顯改善計算效率,因爲它本身內置爲循環運算。
1 |
m.data
<- matrix ( rnorm (100),ncol=10) |
2 |
apply (m.data,1,mean) |
lappy()的處理對象是向量、列表或其它對象,它將向量中的每個元素作爲參數,輸入到處理函數中,最後生成結果的格式爲列表。在R中數據框是一種特殊的列表,所以數據框的列也將作爲函數的處理對象。下面的例子即對一個數據框按列來計算中位數與標準差。
1 |
f.data
<- data.frame (x= rnorm (10),y= runif (10)) |
2 |
lapply (f.data,FUN= function (x) list (median= median (x),sd= sd (x)) |
sapply()可能是使用最爲頻繁的向量化函數了,它和lappy()是非常相似的,但其輸出格式則是較爲友好的矩陣格式。
1 |
sapply (f.data,FUN= function (x) list (median= median (x),sd= sd (x))) |
2 |
class (test) |
tapply()的功能則又有不同,它是專門用來處理分組數據的,其參數要比sapply多一個。我們以iris數據集爲例,可觀察到Species列中存放了三種花的名稱,我們的目的是要計算三種花瓣萼片寬度的均值。其輸出結果是數組格式。
1 |
head (iris) |
2 |
attach (iris) |
3 |
tapply (Sepal.Width,INDEX=Species,FUN=mean) |
與tapply功能非常相似的還有aggregate(),其輸出是更爲友好的數據框格式。而by()和上面兩個函數是同門師兄弟。
另外還有一個非常有用的函數replicate(),它可以將某個函數重複運行N次,常常用來生成較複雜的隨機數。下面的例子即先建立一個函數,模擬扔兩個骰子的點數之和,然後重複運行10000次。
1 |
game
<- function () { |
2 |
n
<- sample (1:6,2,replace=T) |
3 |
return ( sum (n)) |
4 |
} |
5 |
replicate (n=10000, game ()) |
最後一個有趣的函數Vectorize(),它能將一個不能進行向量化運算的函數進行轉化,使之具備向量化運算功能。
VI. 循環與條件
循環
for (n in x) {expr}
R中最基本的是for循環,其中n爲循環變量,x通常是一個序列。n在每次循環時從x中順序取值,代入到後面的expr語句中進行運算。下面的例子即是以for循環計算30個Fibonacci數。
1 |
x
<- c (1,1) |
2 |
for (i in 3:30)
{ |
3 |
x[i]
<- x[i-1]+x[i-2] |
4 |
} |
while (condition) {expr}
當不能確定循環次數時,我們需要用while循環語句。在condition條件爲真時,執行大括號內的expr語句。下面即是以while循環來計算30個Fibonacci數。
1 |
x
<- c (1,1) |
2 |
i
<- 3 |
3 |
while (i
<= 30) { |
4 |
x[i]
<- x[i-1]+x[i-2] |
5 |
i
<- i +1 |
6 |
} |
條件
if (conditon) {expr1} else {expr2}
if語句用來進行條件控制,以執行不同的語句。若condition條件爲真,則執行expr1,否則執行expr2。ifesle()函數也能以簡潔的方式構成條件語句。下面的一個簡單的例子是要找出100以內的質數。
1 |
x
<- 1:100 |
2 |
y
<- rep (T,100) |
3 |
for (i in 3:100)
{ |
4 |
if ( all (i%%(2:(i-1))!=0)){ |
5 |
y[i]
<- TRUE |
6 |
} else {y[i]
<- FALSE |
7 |
} |
8 |
} |
9 |
print (x[y]) |
在上面例子裏,all()函數的作用是判斷一個邏輯序列是否全爲真,%%的作用是返回餘數。在if/else語句中一個容易出現的錯誤就是else沒有放在}的後面,若你執行下面的示例就會出現錯誤。
1 |
logic
= 3 |
2 |
x<- c (2,3) |
3 |
if (logic
== 2){ |
4 |
y
<- x^2 |
5 |
} |
6 |
else { |
7 |
y<-x^3 |
8 |
} |
9 |
show (y) |
一個例子
本例來自於"introduction to Scientific Programming and Simulatoin Using R"一書的習題。有這樣一種賭博遊戲,賭客首先將兩個骰子隨機拋擲第一次,如果點數和出現7或11,則贏得遊戲,遊戲結束。如果沒有出現7或11,賭客繼續拋擲,如果點數與第一次扔的點數一樣,則贏得遊戲,遊戲結束,如果點數爲7或11則輸掉遊戲,遊戲結束。如果出現其它情況,則繼續拋擲,直到贏或者輸。用R編程來計算賭客贏的概率,以決定是否應該參加這個遊戲。
01 |
craps
<- function ()
{ |
02 |
#returns
TRUE if you win, FALSE otherwise |
03 |
initial.roll
<- sum ( sample (1:6,2,replace=T)) |
04 |
if (initial.roll
== 7 || initial.roll == 11) return ( TRUE ) |
05 |
while ( TRUE )
{ |
06 |
current.roll
<- sum ( sample (1:6,2,replace=T)) |
07 |
if (current.roll
== 7 || current.roll == 11) { |
08 |
return ( FALSE ) |
09 |
} else if (current.roll
== initial.roll) { |
10 |
return ( TRUE ) |
11 |
} |
12 |
} |
13 |
} |
14 |
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()
參考資料: http://xccds1977.blogspot.com/2012/02/r_28.html 如何成爲一名黑客 :http://dongxi.net/b14rH How to be a Programmer : http://samizdat.mines.edu/howto/HowToBeAProgrammer.html Teach Yourself Programming in Ten Years : http://norvig.com/21-days.html