鑒於內存的非持久性和容量限制,一個有效的數據處理工具必須能夠使用外部數據:能夠從外部獲取大量的數據,也能夠將處理結果保存。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為操作環境