1. 程式人生 > >R語言-入門綜合

R語言-入門綜合

I. 導論
簡單來講,程式設計是藉助計算機來解決某個問題。學習程式設計的就是訓練我們解決問題的能力。有這樣一種說法:在未來,不會程式設計的人即是文盲。


1 為什麼要學習R程式設計


大部分情況下解決某些問題還需要依賴一些事實或資料,結合資料分析的框架和計算工具來幫助我們決策和判斷。這時候R語言程式設計就會派上用場。例如從大的方面來看,投資方要決定在何處建立風力發電場,就需要採集天氣資料加以建模分析,評估各專案方案。從小的方面來看,個人是否應該購買某個理財產品,你需要獲取過去的市場資訊,模擬未來可能的變化,計算該項資產未來的期望收益和標準差。所以說學習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:
[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
----------------------------
 
# 用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 &lt;= 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()


參考資料:
 
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