R学习笔记(4): 使用外部数据

 

鉴于内存的非持久性和容量限制,一个有效的数据处理工具必须能够使用外部数据:能够从外部获取大量的数据,也能够将处理结果保存。R中提供了一系列的函数进行外部数据处理,从外部数据的类型可以分为文件、数据库、网络等;其中文件操作还可以区分为导入/导出操作和流式操作。

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认为最理想的是如下的格式:

 
 PriceFloorAreaRoomsAgeCent.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)提供了一组函数,实现灵活的指向类似文件对象的接口,以代替文件名的使用。 使用连接的基本步骤:

  1. 创建连接
  2. 打开连接
  3. 操作数据
  4. 关闭连接

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放到内存中,在处理大数据时,就会遇到问题。在处理大数据时,可以采用一下的方法:

  1. 使用数据库 每次从数据库中读取一部分数据进行处理。
  2. 使用连接 尽管文本文件不使用连接也可以操作,但是连接提供了“流”的方式:可以分批进行读写。
  3. 使用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])
  1. 使用虚拟内存 如果大量数据不能拆分,必须一起处理,还可以使用“虚拟内存”。

    包filehash可以将变量存储在磁盘上而不是内存中。

    还可以使用数据库:将文件读入数据库,然后再把数据库装载为环境来代替将文件读入内存的作法。用with()函数可以指定环境。

> dumpDF(read.table("large.txt", header=T), dbName="mydb") > myenv<-db2env(db="mydb")
> with(mydb, z<-y+x ) # 指定mydb为操作环境

Date: 2013-05-16 10:37:42 CST

Author: Holbrook

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0
posted @ 2013-05-16 10:39 心内求法 阅读(...) 评论(...) 编辑 收藏