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: 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()

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM