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)
[/code]
好了,现在我们手头上有一个不熟悉的对象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]])
[/code]
3 读取文本文件
有时候需要读取的数据存放在非结构化的文本文件中,例如电子邮件数据或微博数据。这种情况下只能依靠readLines()函数,将文档转为以行为单位存放的list格式。例如我们希望读取wikipedia的主页html文件的前十行。
[code lang="R"]
data <- readLines('http://en.wikipedia.org/wiki/Main_Page',n=10)
[/code]
另外,scan()也有丰富的参数用来读取非结构化文档。
4 批量读取本地文件
在批量读取文档时一般先将其存放在某一个目录下。先用dir()函数获取目录中的文件名,然后用paste()将路径合成,最后用循环或向量化方法处理文档。例如:
[code lang="R"]
doc.names <- dir("path")
doc.path <- sapply(doc.names,function(names) paste(path,names,sep='/'))
doc <- sapply(doc.path, function(doc) readLines(doc))
[/code]
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)]
[/code]
在字符串的复杂操作中通常会包括正则表达式(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)
[/code]
lappy()的处理对象是向量、列表或其它对象,它将向量中的每个元素作为参数,输入到处理函数中,最后生成结果的格式为列表。在R中数据框是一种特殊的列表,所以数据框的列也将作为函数的处理对象。下面的例子即对一个数据框按列来计算中位数与标准差。
[code lang="R"]
f.data <- data.frame(x=rnorm(10),y=runif(10))
lapply(f.data,FUN=function(x) list(median=median(x),sd=sd(x))
[/code]
sapply()可能是使用最为频繁的向量化函数了,它和lappy()是非常相似的,但其输出格式则是较为友好的矩阵格式。
[code lang="R"]
sapply(f.data,FUN=function(x)list(median=median(x),sd=sd(x)))
class(test)
[/code]
tapply()的功能则又有不同,它是专门用来处理分组数据的,其参数要比sapply多一个。我们以iris数据集为例,可观察到Species列中存放了三种花的名称,我们的目的是要计算三种花瓣萼片宽度的均值。其输出结果是数组格式。
[code lang="R"]
head(iris)
attach(iris)
tapply(Sepal.Width,INDEX=Species,FUN=mean)
[/code]
与tapply功能非常相似的还有aggregate(),其输出是更为友好的数据框格式。而by()和上面两个函数是同门师兄弟。
另外还有一个非常有用的函数replicate(),它可以将某个函数重复运行N次,常常用来生成较复杂的随机数。下面的例子即先建立一个函数,模拟扔两个骰子的点数之和,然后重复运行10000次。
[code lang="R"]
game <- function() {
n <- sample(1:6,2,replace=T)
return(sum(n))
}
replicate(n=10000,game())
[/code]
最后一个有趣的函数Vectorize(),它能将一个不能进行向量化运算的函数进行转化,使之具备向量化运算功能。
VI. 循环与条件
循环
for (n in x) {expr}
R中最基本的是for循环,其中n为循环变量,x通常是一个序列。n在每次循环时从x中顺序取值,代入到后面的expr语句中进行运算。下面的例子即是以for循环计算30个Fibonacci数。
[code lang="R"]
x <- c(1,1)
for (i in 3:30) {
x[i] <- x[i-1]+x[i-2]
}
[/code]
while (condition) {expr}
当不能确定循环次数时,我们需要用while循环语句。在condition条件为真时,执行大括号内的expr语句。下面即是以while循环来计算30个Fibonacci数。
[code lang="R"]
x <- c(1,1)
i <- 3
while (i <= 30) {
x[i] <- x[i-1]+x[i-2]
i <- i +1
}
[/code]
条件
if (conditon) {expr1} else {expr2}
if语句用来进行条件控制,以执行不同的语句。若condition条件为真,则执行expr1,否则执行expr2。ifesle()函数也能以简洁的方式构成条件语句。下面的一个简单的例子是要找出100以内的质数。
[code lang="R"]
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])
[/code]
在上面例子里,all()函数的作用是判断一个逻辑序列是否全为真,%%的作用是返回余数。在if/else语句中一个容易出现的错误就是else没有放在}的后面,若你执行下面的示例就会出现错误。
[code lang="R"]
logic = 3
x<- c(2,3)
if (logic == 2){
y <- x^2
}
else {
y<-x^3
}
show(y)
[/code]
一个例子
本例来自于"introduction to Scientific Programming and Simulatoin Using R"一书的习题。有这样一种赌博游戏,赌客首先将两个骰子随机抛掷第一次,如果点数和出现7或11,则赢得游戏,游戏结束。如果没有出现7或11,赌客继续抛掷,如果点数与第一次扔的点数一样,则赢得游戏,游戏结束,如果点数为7或11则输掉游戏,游戏结束。如果出现其它情况,则继续抛掷,直到赢或者输。用R编程来计算赌客赢的概率,以决定是否应该参加这个游戏。
[code lang="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()))
[/code]
从最终结果来看,赌客赢的概率为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