鉴于内存的非持久性和容量限制,一个有效的数据处理工具必须能够使用外部数据:能够从外部获取大量的数据,也能够将处理结果保存。R中提供了一系列的函数进行外部数据处理,从外部数据的类型可以分为文件、数据库、网络等;其中文件操作还可以区分为导入/导出操作和流式操作。
Table of Contents
1 数据框
前面 仅仅提到:
列表(list)和数据框(data frame)分别是向量和矩阵的泛化——列表允许包含不同类型的元素,甚至可以把对象作为元素;数据框允许每列使用不同类型的元素。对于列表和数据框,其中的元素通常称为分量(components)。
因为外部数据的处理涉及到数据框,这里对列表和数据框进行更详细的说明。
1.1 列表
列表的分量可以是不同的类型,使用list()函数可以创建列表:
> x = list(name="Fred", wife="Mary", no.children=3, child.ages=c(4,7,9)) > x $name [1] "Fred" $wife [1] "Mary" $no.children [1] 3 $child.ages [1] 4 7 9
列表元素可以通过几种不同的索引进行访问:
> x[[1]] [1] "Fred" > x[1][1] $name [1] "Fred" > x["name"][1] $name [1] "Fred" > x$name [1] "Fred"
1.2 数据框
数据框是一种特殊的列表,是和矩阵类似的一种结构。在数据框中, 列可以是不同的对象。 可以把数据框看作是一个 行表示观测个体并且(可能)同时拥有数值变量和 分类变量的 `数据矩阵' ,行和列可以通过矩阵的索引方式进行访问。
下面是例子:
> L3 = LETTERS[1:3] > L3 [1] "A" "B" "C" > d = data.frame(cbind(x = 1, y = 1:10), fac = sample(L3, 10, replace = TRUE)) > d x y fac 1 1 1 B 2 1 2 A 3 1 3 C 4 1 4 C 5 1 5 B 6 1 6 A 7 1 7 A 8 1 8 B 9 1 9 B 10 1 10 A > d$x [1] 1 1 1 1 1 1 1 1 1 1 > d[[2]] [1] 1 2 3 4 5 6 7 8 9 10 > d[1][1] x 1 1 2 1 3 1 4 1 5 1 6 1 7 1 8 1 9 1 10 1
数据框是非常有用的工具。R中还提供了合并数据框的函数。对于两个有相同列的数据框,可以用merge()函数进行合并,可以指定安装哪一个列进行合并:
> x <- data.frame(k1 = c(NA,NA,3,4,5), k2 = c(1,NA,NA,4,5), data = 1:5) > y <- data.frame(k1 = c(NA,2,NA,4,5), k2 = c(NA,NA,3,4,5), data = 1:5) > x k1 k2 data 1 NA 1 1 2 NA NA 2 3 3 NA 3 4 4 4 4 5 5 5 5 > y k1 k2 data 1 NA NA 1 2 2 NA 2 3 NA 3 3 4 4 4 4 5 5 5 5 > merge(x,y) k1 k2 data 1 4 4 4 2 5 5 5 > merge(x,y,by='k1') k1 k2.x data.x k2.y data.y 1 4 4 4 4 4 2 5 5 5 5 5 3 NA 1 1 NA 1 4 NA 1 1 3 3 5 NA NA 2 NA 1 6 NA NA 2 3 3 > merge(x, y, by = c("k1","k2")) k1 k2 data.x data.y 1 4 4 4 4 2 5 5 5 5 3 NA NA 2 1
1.3 编辑数据框
前面提到data.entry()函数可以打开数据编辑器,但是不适用于数据框。如果用data.entry()修改了数据框,会转换成列表(list)类型。
编辑数据框需要使用edit()函数:
> xnew = edit(xold) edit()函数只是编辑,并不赋值。如果要直接修改数据框,需要使用如下的形式: > x = edit(x) > fix(x) #等价于上面的形式
2 CSV文件的导入导出
R中处理文本文件主要是使用read.table()函数将数据读入数据框;如果要对导入方式进行复杂的控制,开可以使用古老的scan()。 注:scan处理处理文件导入外,还可以直接接受键盘输入。
在本系列的一开始,我们提到了工作空间,可以使用函数getwd()和setwd()来获取/设置工作空间目录;使用list.files()查看当前目录下的文件。
对于工作空间中的文本文件,可以使用相对路径操作,其他文件要使用绝对路径。
2.1 文件格式
R支持丰富的文件格式,支持CSV、FIX、DIF、XML等文本格式和DBF、XLS、HDF5、netCDF等二进制格式。
对于CSV文件,R认为最理想的是如下的格式:
Price | Floor | Area | Rooms | Age | Cent.heat | |
---|---|---|---|---|---|---|
01 | 52.00 | 111.0 | 830 | 5 | 6.2 | no |
02 | 54.75 | 128.0 | 710 | 5 | 7.5 | no |
03 | 57.50 | 101.0 | 1000 | 5 | 4.2 | no |
04 | 57.50 | 131.0 | 690 | 6 | 8.8 | no |
05 | 59.75 | 93.0 | 900 | 5 | 1.9 | yes |
即,第一行为数据框各分量的名字,随后的每一行第一项为行标签,其余为数据。
如果不符合这样的默认格式,需要在导入函数中指定特定的参数。
2.2 read.table()和write.table()
最常用的方式是使用read.table()函数和write.table()处理CSV文件的导入导出。函数read()和write()只能处理矩阵或向量的特定列,而read.table()和write.table()可以处理包含行、列标签的数据框。
read.talbe()函数读取文件并返回一个数据框:
read.table(file, header = FALSE, sep = "", quote = "\"'", dec = ".", row.names, col.names, as.is = !stringsAsFactors, na.strings = "NA", colClasses = NA, nrows = -1, skip = 0, check.names = TRUE, fill = !blank.lines.skip, strip.white = FALSE, blank.lines.skip = TRUE, comment.char = "#", allowEscapes = FALSE, flush = FALSE, stringsAsFactors = default.stringsAsFactors(), fileEncoding = "", encoding = "unknown", text)
一些主要的参数:
- file : 要处理的文件。可以用字符串指定文件名,也可以使用函数,如:file('file.dat',encoding='utf-8')
- header:首行是否为字段名。如果不指定,read.table()会根据行标签进行判断,即如果首行比下面的行少一列,就是header行
- col.names: 如果指定,则用指定的名称替代首行中的列名称
- sep:指定分隔符。默认为空白符(空格,制表符,换行符等)。可以指定为' ', '\t'等
- quote:指定字符串分隔符,如" 或 '
- na.strings: 指定缺损值。默认为NA
- fill :文件中是否忽略了行尾字段。如果有,必须指定为 TRUE
- strip.white:是否去除字符串字段首尾的空白
- blank.lines.skip:是否忽略空白行,默认为TRUE。如果要指定为FALSE,需要同时指定 fill = TRUE 才有效
- colClasses:指定每个列的数据类型
- comment.char : 注释符。默认使用#作为注释符号,如果文件中没有注释,指定comment.char = "" 会比较安全 (也可能让速度比较快)
为了使用方便,read.table()函数还提供了一些变体,这些变体为read.table()的一些参数设定了默认值:
read.csv(file, header = TRUE, sep = ",", quote = "\"", dec = ".", fill = TRUE, comment.char = "", ...) read.csv2(file, header = TRUE, sep = ";", quote = "\"", dec = ",", fill = TRUE, comment.char = "", ...) read.delim(file, header = TRUE, sep = "\t", quote = "\"", dec = ".", fill = TRUE, comment.char = "", ...) read.delim2(file, header = TRUE, sep = "\t", quote = "\"", dec = ",", fill = TRUE, comment.char = "", ...) write.table()的参数要少一些: write.table(x, file = "", append = FALSE, quote = TRUE, sep = " ", eol = "\n", na = "NA", dec = ".", row.names = TRUE, col.names = TRUE, qmethod = c("escape", "double"), fileEncoding = "")
一些参数的说明:
- x 要写入的对象的名称
- file 文件名(缺省时对象直接被“写”在屏幕上)
- append 是否为增量写入
- quote 一个逻辑型或者数值型向量:如果为TRUE,则字符型变量和因子写在双引 号""中;若quote是数值型向量则代表将欲写在""中的那些列的列标。(两种 情况下变量名都会被写在""中;若quote = FALSE则变量名不包含在双引号中)
- sep 文件中的字段分隔符
- eol 指定行尾符,默认为'\n'
- na 表示缺失数据的字符
- dec 用来表示小数点的字符
- row.names 一个逻辑值,决定行名是否写入文件;或指定要作为行名写入文件的字符型 向量
- col.names 一个逻辑值(决定列名是否写入文件);或指定一个要作为列名写入文件中 的字符型向量
- qmethod 若quote=TRUE,则此参数用来指定字符型变量中的双引号"如何处理: 若参数值为"escape" (或者"e",缺省)每个"都用\"替换;若值为"d"则每 个"用""替换
类似的,write.table()也提供了一些变体:
write.csv(…)
write.csv2(…)
示例: 将前面的例子保存为工作空间下的文件,然后执行命令:
> x = read.table('sample.csv',sep='\t') > x V1 V2 V3 V4 V5 V6 V7 1 NA Price Floor Area Rooms Age Cent.heat 2 1 52.00 111.0 830 5 6.2 no 3 2 54.75 128.0 710 5 7.5 no 4 3 57.50 101.0 1000 5 4.2 no 5 4 57.50 131.0 690 6 8.8 no 6 5 59.75 93.0 900 5 1.9 yes
使用fix(x)编辑数据后,用write.table(x,'sample1.csv')保存。
2.3 scan()和cat()
read.table()很方便,但是处理大矩阵时的效率很低,比如你可以实验一下一个不太大(200x2000)的矩阵操作:
>write.table(matrix(rnorm(200*2000), 200), "matrix.dat", row.names=F, col.names=F) > A <- as.matrix(read.table("matrix.dat")) #需要大概7秒
read.table()调用了scan()读取文件,然后对结果进行处理。如果直接使用scan()读取,效率会更高:
> A <- matrix(scan("matrix.dat", n = 200*2000), 200, 2000, byrow = TRUE) #需要大概2秒
当矩阵的规模更大时,这种差异会更加突出。 scan()函数比read.table()要更加灵活,一个非常主要的区别是scan()可以指定变量的类型,避免类型校验带来的开销:
scan(file = "", what = double(), nmax = -1, n = -1, sep = "", quote = if(identical(sep, "\n")) "" else "'\"", dec = ".", skip = 0, nlines = 0, na.strings = "NA", flush = FALSE, fill = FALSE, strip.white = FALSE, quiet = FALSE, blank.lines.skip = TRUE, multi.line = TRUE, comment.char = "", allowEscapes = FALSE, fileEncoding = "", encoding = "unknown", text)
同理,cat()函数也比write.table()灵活:
cat(... , file = "", sep = " ", fill = FALSE, labels = NULL, append = FALSE)
3 使用连接(connection)
R中的连接(Connections)提供了一组函数,实现灵活的指向类似文件对象的接口,以代替文件名的使用。 使用连接的基本步骤:
- 创建连接
- 打开连接
- 操作数据
- 关闭连接
R中通过函数 showConnections() 可以列出当前用户打开的连接。 通过函数 showConnections(all = TRUE) 则可以查看所有连接的汇总信息,包括已经关闭或终止的连接。
3.1 连接的类型
R可以把很多种数据源都看做连接,包括:
- 文件 file()函数创建一个文件连接,可以打开文本文件或二进制文件。对于gzip或bzip2压缩的文件,可以使用gzfile()和bzfile()函数创建连接。
- 标准I/O R中可以使用stdin()、stdout()、stderr()函数建立到标准I/O的连接。这些连接不需要打开就能直接使用,而且不能关闭。
- 字符向量 R中甚至允许以一个字符向量作为输入或输出。使用textConnection()函数创建到字符向量的连接。
- 管道(Pipes) UNIX中的管道有着非凡重要的意义,可以非常简单的实现进程间通信。R函数pipe()可以创建管道连接。
- URL
URL 类型的 http://,ftp:// 和 //localhost/ 可以通过函数 url 读内容。为方便起见,file 也可以 接受这种文件规范和调用url。
- socket
函数socketConnection()可以创建socket连接
3.2 输出到连接
直接看例子:
zz <- file("ex.data", "w") # 打开一个输出文件连接 cat("TITLE extra line", "2 3 5 7", "", "11 13 17", file = zz, sep = "\n") cat("One more line\n", file = zz) close(zz) ## 使用管道(Unix)在输出中把小数点转换成逗号 ## R字符串和(可能)SHELL脚本中都需要把 \ 写两次 zz <- pipe(paste("sed s/\\\\./,/ >", "outfile"), "w") cat(format(round(rnorm(100), 4)), sep = "\n", file = zz) close(zz) ## 现在查看输出文件: file.show("outfile", delete.file = TRUE) ## 捕获R输出:使用 help(lm) 里面的例子 zz <- textConnection("ex.lm.out", "w") sink(zz) example(lm, prompt.echo = "> ") sink() close(zz) ## 现在 `ex.lm.out' 含有需要进一步处理的输出内容 ## 查看里面的内容,如 cat(ex.lm.out, sep = "\n")
3.3 从连接输入
从连接读入数据的基本函数是scan 和 readLines。这些函数有个以字符串作为输入的参数,在 函数调用时会打开一个文件连接,但显式地打开文件连接允许一个文件 可以连续地以不同格式读入。 调用 scan 的其它函数也可以使用连接, 特别是 read.table。 一些简单的例子如下:
## 读入前面例子中创建的文件 readLines("ex.data") unlink("ex.data") ## 读入当前目录的清单(Unix) readLines(pipe("ls -1")) # 从输入文件中去掉拖尾的逗号。 # 假定我们有一个包含如下`数据'的文件 450, 390, 467, 654, 30, 542, 334, 432, 421, 357, 497, 493, 550, 549, 467, 575, 578, 342, 446, 547, 534, 495, 979, 479 # 然后通过如下命令读入 scan(pipe("sed -e s/,$// data"), sep=",")
从连接输入数据还可以使用压栈操作。类似于C语言中的ungetc函数,R中的pushBack()函数可以把任意数据压入给连接。压入后的数据以堆栈方式存储(FILO)。栈不为空时从栈中取数据,栈为空才从连接输入数据。
例子如下:
> zz <- textConnection(LETTERS) > readLines(zz, 2) [1] "A" "B" > scan(zz, "", 4) Read 4 items [1] "C" "D" "E" "F" > pushBack(c("aa", "bb"), zz) > scan(zz, "", 4) Read 4 items [1] "aa" "bb" "G" "H" > close(zz)
压栈操作仅适用于文本输入模式的连接。
3.4 二进制连接
在打开连接时用'b'设置二进制方式,如'rb','wb'等,则可以使用readBin()和writeBin()函数进行二进制方式的读写。函数说明如下:
readBin(con, what, n = 1, size = NA, endian = .Platform$endian) writeBin(object, con, size = NA, endian = .Platform$endian)
其中:
- con 要打开的连接。如果给定的是字符串,它会被假定是文件名字。
- what 说明向量的类型/模式。比如 numeric,integer,logical,character, complex 或 raw 。可以用函数如integer()或字符串如'integer'作为参数。
- n 要读入的最大元素数量
- size 指定字节数。比如,通过设定size可以读写16位的整数或单精度的实数。
- object 要写入的对象,必须是原子型向量对象,也就是没有属性的 numeric,integer,logical,character, complex 或 raw 模式的向量。默认情况下,这些以 和内存里面完全一样字节流的写入文件的。
4 一些特定的文件格式
DBF文件:使用read.dbf()和write.dbf()函数进行读写
XLS文件:最好转换成csv再导入,如果一定要直接使用XLS,可以用RODBC操作,参考后面的数据库部分;
FIX文件:使用read.fwf()、 read.fortran()导入;
DIF文件:使用read.DIF()导入;也可以使用read.DIF('clipboard')读取剪贴板中的数据;
XML文件:包XML 提供了对xml文件的支持。
HDF5文件:使用包hdf5处理
netCDF文件:使用包RNetCDF处理
foreign包提供了一些函数,可以导入EpiInfo, Minitab, S-PLUS, SAS, SPSS, Stata, Systat、Octave等软件的数据文件;可以导出Stata和SPSS的数据文件。
5 使用关系数据库
R中提供了不同抽象层次上的连接数据库的包,比如底层的DBI ,上层的RMySQL、 ROracle、 RSQlite、RODBC等。
此外还有一个把R嵌入PostgreSQL 的项目:http://www.joeconway.com/plr/ 。
5.1 包 DBI 和 RMySQL
MySQL是很常用的开源数据库。CRAN的包RMySQL提供了对MySQL数据库的访问支持:
- 使用dbDriver("MySQL")获取数据库连接管理对象。类似的,其他的包中也提供了dbDriver("Oracle") , dbDriver("SQLite")的方式。
- 调用dbConnect打开一个数据库连接
- 使用dbSendQuery()或 dbGetQuery()发送查询。其中dbGetQuery 传送查询语句, 把结果以数据框形式返回。dbSendQuery 传送查询,返回的结果是 继承"DBIResult"的一个子类的对象。"DBIResult" 类 可用于取得结果,而且还可以通过调用 dbClearResult 清除结果。
- 使用fetch()函数 获得查询结果的部分或全部行,并以列表返回。 函数 dbHasCompleted 确定是否所有行已经获得了, 而 dbGetRowCount 返回结果中行的数目。
- 函数dbReadTable 和 dbWriteTable 可以在R数据框和数据库表之间传递数据,数据框的行名字映射到 MySQL 表的 rownames 字段。
- dbDisconnect()用于关闭数据库连接
下面是例子:
> library(RMySQL) # will load DBI as well ## 打开一个MySQL数据库的连接 > con <- dbConnect(dbDriver("MySQL"), dbname = "test") ## 列出数据库中表 > dbListTables(con) ## 把一个数据框导入到数据库,删除任何已经存在的拷贝 > data(USArrests) > dbWriteTable(con, "arrests", USArrests, overwrite = TRUE) TRUE > dbListTables(con) [1] "arrests" ## 获得整个表 > dbReadTable(con, "arrests") Murder Assault UrbanPop Rape Alabama 13.2 236 58 21.2 Alaska 10.0 263 48 44.5 Arizona 8.1 294 80 31.0 Arkansas 8.8 190 50 19.5 ... ## 从导入的表中查询 > dbGetQuery(con, paste("select row_names, Murder from arrests", "where Rape > 30 order by Murder")) row_names Murder 1 Colorado 7.9 2 Arizona 8.1 3 California 9.0 4 Alaska 10.0 5 New Mexico 11.4 6 Michigan 12.1 7 Nevada 12.2 8 Florida 15.4 > dbRemoveTable(con, "arrests") > dbDisconnect(con)
5.2 RODBC
CRAN 里面的包 RODBC 提供了 ODBC的访问接口:
- odbcConnect 或 odbcDriverConnect (在Windows图形化界面下,可以通过对话框选择数据库) 可以打开一个连接,返回一个用于随后数据库访问的控制(handle)。 打印一个连接会给出ODBC连接的一些细节,而调用 odbcGetInfo 会给出客户端和服务器的一些细节信息。
- 在一个连接中的表的细节信息可以通过函数 sqlTables 获得。
- 函数 sqlSave 会把 R 数据框复制到一个数据库的表中, 而函数 sqlFetch 会把一个数据库中的表拷贝到 一个 R 的数据框中。
- 通过sqlQuery进行查询,返回的结果是 R 的数据框。(sqlCopy把一个 查询传给数据库,返回结果在数据库中以表的方式保存。) 一种比较好的控制方式是首先调用 odbcQuery, 然后 用 sqlGetResults 取得结果。后者可用于一个循环中 每次获得有限行,就如函数 sqlFetchMore 的功能。
- 连接可以通过调用函数 close 或 odbcClose 来关闭。 没有 R 对象对应或不在 R 会话后面的连接也可以调用这两个函数来关闭, 但会有警告信息。
代码示例:
> library(RODBC) ## 让函数把名字映射成小写 > channel <- odbcConnect("testdb", uid="ripley", case="tolower") ## 把一个数据框导入数据库 > data(USArrests) > sqlSave(channel, USArrests, rownames = "state", addPK = TRUE) > rm(USArrests) ## 列出数据库的表 > sqlTables(channel) TABLE_QUALIFIER TABLE_OWNER TABLE_NAME TABLE_TYPE REMARKS 1 usarrests TABLE ## 列出表格 > sqlFetch(channel, "USArrests", rownames = "state") murder assault urbanpop rape Alabama 13.2 236 58 21.2 Alaska 10.0 263 48 44.5 ... ## SQL查询,原先是在一行的 > sqlQuery(channel, "select state, murder from USArrests where rape > 30 order by murder") state murder 1 Colorado 7.9 2 Arizona 8.1 3 California 9.0 4 Alaska 10.0 5 New Mexico 11.4 6 Michigan 12.1 7 Nevada 12.2 8 Florida 15.4 ## 删除表 > sqlDrop(channel, "USArrests") ## 关闭连接 > odbcClose(channel)
作为 Windows下面用 ODBC 连接 Excel电子表格的一个简单例子, 我们可以如下读取电子表格:
> library(RODBC) > channel <- odbcConnectExcel("bdr.xls") ## 列出电子表格 > sqlTables(channel) TABLE_CAT TABLE_SCHEM TABLE_NAME TABLE_TYPE REMARKS 1 C:\\bdr NA Sheet1$ SYSTEM TABLE NA 2 C:\\bdr NA Sheet2$ SYSTEM TABLE NA 3 C:\\bdr NA Sheet3$ SYSTEM TABLE NA 4 C:\\bdr NA Sheet1$Print_Area TABLE NA ## 获得表单1的内容,可以用下面任何一种方式 > sh1 <- sqlFetch(channel, "Sheet1") > sh1 <- sqlQuery(channel, "select * from [Sheet1$]")
注意,数据库表的规范和 sqlTables 返回的名字是不一样的: sqlFetch 可以映射这种差异。
6 网络接口及外部工具
R对于在网络连接的底层水平上交换数据,提供的支持非常有限。但是也提供了一些支持:
函数 make.socket, read.socket,write.socket 和 close.socket 提供了socket支持;
函数 download.file 通过FTP或HTTP读取来自网络资源的文件,然后写入到一个文件中;
函数 read.table 和 scan 都可以直接从一个URL读取内容,它们要么显式地用 url 打开一个连接,要么暗含地给 file 参数设定一个URL,不需要保存文件到本地;
CORBA包 提供了CORBA协议的支持;
此外对于DCOM协议也有第三方的支持。
按照UNIX哲学,我们不建议在R中直接使用这些接口,而是交给外部工具来做。这里举一个外部工具的例子:
> files <- system("ls x*", intern=T) #一定要指定 intern
7 处理大数据
前面介绍了R使用外部数据的一些方法,通常这已经够用了。但是从外部获取的数据会被R放到内存中,在处理大数据时,就会遇到问题。在处理大数据时,可以采用一下的方法:
- 使用数据库 每次从数据库中读取一部分数据进行处理。
- 使用连接 尽管文本文件不使用连接也可以操作,但是连接提供了“流”的方式:可以分批进行读写。
- 使用UNIX工具 可以使用Unix中 grep、awk 和 wc 等实用工具对文本文件进行预处理,然后在读入到R中,比如:
> howmany <- as.numeric(system ("grep -c ',C,' file.dat")) > totalrows <- as.numeric(strsplit(system("wc -l Week.txt", intern=T), split=" ")[[1]][1])
- 使用虚拟内存 如果大量数据不能拆分,必须一起处理,还可以使用“虚拟内存”。
包filehash可以将变量存储在磁盘上而不是内存中。
还可以使用数据库:将文件读入数据库,然后再把数据库装载为环境来代替将文件读入内存的作法。用with()函数可以指定环境。
> dumpDF(read.table("large.txt", header=T), dbName="mydb") > myenv<-db2env(db="mydb") > with(mydb, z<-y+x ) # 指定mydb为操作环境