用R語言做資料清理
資料的清理
如同列夫托爾斯泰所說的那樣:“幸福的家庭都是相似的,不幸的家庭各有各的不幸”,糟糕的噁心的資料各有各的糟糕之處,好的資料集都是相似的。一份好的,乾淨而整潔的資料至少包括以下幾個要素:
1、每一個觀測變數構成一列
2、每一個觀測物件構成一行
3、每一個型別的觀測單元構成一個表
就像我們最常接觸的鳶尾花資料:
- ## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
-
## 1 5
.1 3.5 1.4 0.2 setosa
- ## 2 4 .9 3.0 1.4 0.2 setosa
- ## 3 4 .7 3.2 1.3 0.2 setosa
- ## 4 4 .6 3.1 1.5 0.2 setosa
-
## 5 5
.0 3.6 1.4 0.2 setosa
每一列就是觀測的指標:花瓣長度,花瓣寬度,萼片長度,萼片寬度,種類;每一行就是一株鳶尾花的觀測值,構成整張表的元素就是四個數值變數,一個分類分類變數。
然而出於排版的考慮我們抓下來的資料往往不是那麼的友好,比如說我們可以看到的資料通常是這樣的:
- ## religion <10k 10k-50k 50k-100k
- ## 1 Agnostic 12 31 23
- ## 2 Buddhist 58 43 43
- ## 3 Catholic 79 56 23
而不是:
- ## religion income freq
- ## 1 Agnostic <10k 12
- ## 2 Agnostic 10k-50k 58
- ## 3 Agnostic 50k-100k 79
-
## 4 Buddhist <10k 31
當然,除了這種把列表每一列代表一些數值這種情況外,還有多個變數儲存為一列(比如列表不僅以"<10k","10k-50k","50k-100k"做表頭,甚至還加上性別資訊"m<10k","m10k-50k","m50k-100k","f<10k","f10k-50k","f50k-100k",其中m代表男性,f代表女性),還有更過分的將列表的變數不僅儲存在列中,行中也有統計變數。
面對這些不好的table,我們首先要做的就是資料管理,將資料整理為一個乾淨的資料集。
資料管理
按照en:DAMA的定義:“資料資源管理,致力於發展處理企業資料生命週期的適當的建構、策略、實踐和程式”。這是一個高層而包含廣泛的定義,而並不一定直接涉及資料管理的具體操作(如關係資料庫的技術層次上的管理)。我們這裡主要講述對於資料的變數命名與資料的合併,旨在方便資料共享。
資料管理首先要做的就是大致上瞭解你的資料,比如有什麼樣的變數,每一行大致長成什麼樣,最常用的就是head(),tail().
我們要做的基本上就是這麼幾項工作:
-
給每一個變數命名,而不是V1,V2,如果有必要可以給出code book。
-
每個變數名最好具有可讀性,除非過長,否則不要用縮寫,例如AgeAtDiagnosis這個命名遠好於AgeDx。
-
通常來說,最好將資料放在一張表裡面,如果因為資料過多,專案過雜,分成了幾張表。那麼一定需要有一列使得這些表之間能夠連線起來,但儘量避免這樣做。
-
我們以UCI的Human Activity Recognition Using Smartphones Data Set 為例來看看資料是如何變成一個基本符合要求的資料。這個資料我們已經下載下來了,其中關於資料的詳細資訊可以參閱read me文件,由於UCI的資料通常都是一個基本合乎規範的資料集(主要是指它的資料集的變數名都是以V1,V2來命名的)加上一個code book。那麼我們看看各個資料的名稱(在feature檔案裡)
我們可以看到各個特徵的名稱直接標在資料上是非常不友善的,我們為了讓他具有可讀性,我們以展示在我們眼前的6個數據為例:
variablename <- head(name)# 將標籤中的大寫字母轉為小寫,我們這裡沒有所以不再賦值,如果需要全變為大寫,可以使用touppertolower(variablename$V2)
# 將變數名分離成3部分splitNames <- strsplit(variablename$V2, "-")splitNames[[1]]
## [1] "tBodyAcc" "mean()" "X"
# 將變數名合成有意的名稱named <- function(x) { rr <- paste(x[2], x[1], "-", x[3], sep = "") chartr("()", "of", rr) } sapply(splitNames, named)
用這樣的名字給資料集命名就感覺舒服多了,我們將一些R中對字串常用的操作函式總結如下,方便我們對資料名稱的修改:
-
sub:替換字串中的第一個模式為設定模式(pattern).
-
gsub:全域性替換字串中的相應模式
-
grep,grepl:這兩個函式返回向量水平的匹配結果,grep僅返回匹配項的下標,而grepl返回所有的查詢結果,並用邏輯向量表示有沒有找到匹配。
-
nchar:統計字串單字數目
-
substr:取子串
-
paste:將字串連結起來,sep引數可以設定連線符
-
str_trim:去掉字串空格
變數的名稱建議滿足如下要求:
-
英文變數名儘可能用小寫
-
儘可能的描述清楚變數特徵 (Diagnosis versus Dx)
-
不要太複雜
-
不要有下劃線、點、空格
字元型變數應該滿足:
-
是因子型別的應該轉化為factor
-
因子儘可能具有一定的描述性 (例如:如果0/1表示真假,那麼用TRUE/FALSE代替0/1;在表示性別時用Male/Female代替M/F)
接下來我們討論資料集的合併,主要使用函式merge。
我們以下面兩個資料集的合併為例:
df1 <- data.frame(id = sample(1:10), reviewer_id = sample(5:14), time_left = sample(1321:1330), x = rnorm(10))df2 <- data.frame(id = sample(1:10), answer = rep("B", 10), time_left = sample(321:330), y = rnorm(10)) head(df1, n = 3)
- ## id reviewer_id time_left x
- ## 1 3 9 1326 -0.9232
- ## 2 10 5 1322 2.5069
- ## 3 1 14 1330 2.2478
head(df2, n = 3)
- ## id answer time_left y
- ## 1 1 B 329 0.8180
- ## 2 10 B 327 1.4639
- ## 3 9 B 323 0.8141
merge函式呼叫格式為:
- merge(x, y, by = intersect(names(x), names(y)),
- by.x = by, by.y = by, all = FALSE, all.x = all, all.y = all,
- sort = TRUE, suffixes = c(".x",".y"),
- incomparables = NULL, ...)
引數說明:
-
x,y:兩個資料框
-
by, by.x, by.y:指定用於合併的列的名稱。
-
all,all.x,all.y:預設的all = FALSE相當於自然連線, 或者說是內部連結. all.x = TRUE是一個左連線, all.y = TRUE是一個又連線, all = TRUE 相當於一個外部連結.
仔細觀察下面3個例子你就會發現其中的奧祕:
mergedData <- merge(df1,df2,by.x="reviewer_id",by.y="id",all=TRUE) head(mergedData)
- ## reviewer_id id time_left.x x answer time_left.y y
- ## 1 1 NA NA NA B 329 0.8180
- ## 2 2 NA NA NA B 330 -0.7706
-
## 3 3
NA NA NA B 325 -0.4851
mergedData <- merge(df1,df2,by.x="id",by.y="id",all=TRUE) head(mergedData)
- ## id reviewer_id time_left.x x answer time_left.y y
- ## 1 1 14 1330 2 .24783 B 329 0.8180
- ## 2 2 12 1324 1 .03181 B 330 -0.7706
- ## 3 3 9 1326 -0.92317 B 325 -0.4851
-
## 4 4 7 1321
-0.07841 B 322 0.1801
mergedData2 <- merge(df1,df2,all=TRUE) head(mergedData2)
在plyr包中還提供了join,join_all,arrange等函式來實現表的連線,但我想merge這個函式已經足夠用了,所以我們不在多說。當然,在極少數特別好的情況下(比如列的變數是一致的,或者行的觀測個體是一致的時候)rbind,cbind也是有用的。
有些時候我們會遇到一些特殊的字串:日期。R中提供了各式各樣的函式來處理時間:
Sys.setlocale("LC_TIME", "C")
## [1] "C"
x <- c("1jan1960", "2jan1960", "31mar1960", "30jul1960")z <- as.Date(x, "%d%b%Y") format(z, "%a %b %d")
## [1] "Fri Jan 01" "Sat Jan 02" "Thu Mar 31" "Sat Jul 30"
weekdays(z)
## [1] "Friday" "Saturday" "Thursday" "Saturday"
julian(z)
transform(z, weekend = as.POSIXlt(z, format = "%Y/%m/%d")$wday %in% c(0, 6))
- ## X_data weekend
- ## 1 1960-01-01 FALSE
- ## 2 1960-01-02 TRUE
- ## 3 1960-03-31 FALSE
- ## 4 1960-07-30 TRUE
資料操作與整合
說到資料操作,這也是一個十分寬泛的話題,在這裡我們就以下4個方面進行介紹:
-
資料的篩選,過濾:根據一些特定條件選出或者刪除一些觀測
-
資料的變換:增加或者修改變數
-
資料的彙總:分組計算資料的和或者均值
-
資料的排序:改變觀測的排列順序
然而在進行這一切之前首先要做的就是了解你的資料,我們以世界銀行的資料Millennium Development Goals為例,來一步步演示如何進行資料操作:
if (!file.exists("C:/Users/yujun/Documents/MDG_Data.csv")) {
download.file("http://databank.worldbank.org/data/download/MDG_csv.zip","F:/MDG.zip") unzip("F:/MDG.zip") }MDstats<-read.csv("C:/Users/yujun/Documents/MDG_Data.csv")
首先先來看一部分資料:
head(MDstats)
- ## Country.Name Country.Code
- ## 1 Afghanistan AFG
- ## 2 Afghanistan AFG
-
## 3 Afghanistan AFG
tail(MDstats)
- ## Country.Name Country.Code
- ## 33093 Zimbabwe ZWE
- ## 33094 Zimbabwe ZWE
- ## 33095 Zimbabwe ZWE
-
## 33096 Zimbabwe ZWE
我們顯然發現了這不是一個tidy data,那麼我們先將其變換為我們喜歡的tidy data,之後再看看資料摘要及資料集各單元的屬性:
- ## countryname countrycode
- ## 1 Afghanistan AFG
- ## 2 Afghanistan AFG
- ## 3 Afghanistan AFG
- ## 4 Afghanistan AFG
- ## 5 Afghanistan AFG
- ## 6 Afghanistan AFG
- ## indicatorname
- ## 1 Adolescent fertility rate (births per 1,000 women ages 15-19)
- ## 2 Agricultural support estimate (% of GDP)
我們可以看看各個數值資料的分位數:
quantile(MDstatsMelt$value,na.rm=TRUE)
- ## 0% 25% 50% 75% 100%
- ## -9.431e+08 1.054e+01 5.060e+01 9.843e+01 7.526e+13
看看各個國家的統計資料有多少:
table(MDstatsMelt$countrycode)
- ##
- ## ABW ADO AFG AGO ALB ARB ARE ARG ARM ASM ATG AUS AUT AZE BDI
- ## 3216 3216 3216 3216 3216 3216 3216 3216 3216 3216 3216 3216 3216 3216 3216
-
## BEL BEN BFA BGD BGR BHR BHS BIH BLR BLZ BMU BOL BRA BRB BRN
看看缺失值:
sum(is.na(MDstatsMelt$value)) #總的缺失值
## [1] 495519
colSums(is.na(MDstatsMelt)) #每一列的缺失值
- ## countryname countrycode indicatorname indicatorcode year
- ## 0 0 0 0 0
- ## value
- ## 495519
# 如果我們用回tidy前的資料集,那麼這個函式會顯得比較有用colSums(is.na(MDstats))
- ## Country.Name Country.Code Indicator.Name Indicator.Code X1990
- ## 0 0 0 0 23059
- ## X1991 X1992 X1993 X1994 X1995
- ## 22293 21672 21753 21491 20970
- ## X1996 X1997 X1998 X1999 X2000
-
## 20680 20448 20419 19933 18822
# 等價的處理方式stat <- function(x) {
sum(is.na(x)) } tapply(MDstatsMelt$value, MDstatsMelt$year, stat)
- ## X1990 X1991 X1992 X1993 X1994 X1995 X1996 X1997 X1998 X1999 X2000 X2001
- ## 23059 22293 21672 21753 21491 20970 20680 20448 20419 19933 18822 19598
- ## X2002 X2003 X2004 X2005 X2006 X2007 X2008 X2009 X2010 X2011 X2012 X2013
- ## 19119 19478 19269 18704 19044 18641 19256 19162 18756 20360 21967 30625
統計某個國家的統計資料佔總統計數目的多少
table(MDstatsMelt$countryname %in% c("China"))
- ##
- ## FALSE TRUE
- ## 791136 3216
prop <- table(MDstatsMelt$countryname %in% c("China"))[2]/sum(table(MDstatsMelt$countryname %in% c("China")))prop
- ## TRUE
- ## 0.004049
看看資料集的大小:
object.size(MDstatsMelt)
## 22301832 bytes
print(object.size(MDstatsMelt),units="Mb")
## 21.3 Mb
至此,我們可以說我們對資料有了一定的瞭解。另外值得一提的是,對於某些特定的資料,也許xtabs,ftable是有用的。
資料的篩選
要提取相應內容的資料,最為常用的就是提取相應元素,比如提取某個元素,提取某一行,某一列。我們通過下面下面的例子來學習:
data<-data.frame(a=sample(1:10),b=rep(c("a","b"),each=5),cdf=rnorm(10))data
- ## a b cdf
- ## 1 1 a 0.5755
- ## 2 10 a 0.8087
- ## 3 2 a 0.9810
- ## 4 7 a -0.4635
-
## 5 4 a 0.5094
#提取相應元素data[2,1]
## [1] 10
data[[1]][[2]]
## [1] 10
data[[c(1,2)]]
## [1] 10
data$a[2]
## [1] 10
#提取某一列data[[3]]
- ## [1] 0.5755 0.8087 0.9810 -0.4635 0.5094 1.0514 -1.5338 1.0047
- ## [9] 1.0004 -1.3566
data$cdf
- ## [1] 0.5755 0.8087 0.9810 -0.4635 0.5094 1.0514 -1.5338 1.0047
- ## [9] 1.0004 -1.3566
data$c
- ## [1] 0.5755 0.8087 0.9810 -0.4635 0.5094 1.0514 -1.5338 1.0047
- ## [9] 1.0004 -1.3566
data[["c"]]
## NULL
data[["c", exact = FALSE]]
- ## [1] 0.5755 0.8087 0.9810 -0.4635 0.5094 1.0514 -1.5338 1.0047
- ## [9] 1.0004 -1.3566
資料的篩選還有一個最為常用的的就是移除缺失值:
data<-data.frame(a=c(sample(1:5),NA,NA,sample(6:10)),b=c(rep(c("a","b"),each=5),NA,NA),cdf=rnorm(12))data
- ## a b cdf
- ## 1 5 a -0.276400
-
## 2 1 a -1.861240
good <- complete.cases(data)data[good, ]
- ## a b cdf
- ## 1 5 a -0.2764
- ## 2 1 a -1.8612
-
## 3 3 a -2.0280
bad <- as.data.frame(is.na(data))data[!(bad$a|bad$b|bad$c),]
- ## a b cdf
- ## 1 5 a -0.2764
-
## 2 1 a -1.8612
資料篩選有時是為了獲得符合條件的資料:
X <- data.frame("var1"=sample(1:5),"var2"=sample(6:10),"var3"=sample(11:15))X <- X[sample(1:5),]; X$var2[c(1,3)] = NAX
- ## var1 var2 var3
- ## 2 5 NA 13
- ## 5 3 6 15
- ## 1 2 NA 12
- ## 3 1 8 11
- ## 4 4 9 14
X[(X$var1 <= 3 & X$var3 > 11),]
- ## var1 var2 var3
- ## 5 3 6 15
- ## 1 2 NA 12
subset(X,(X$var1 <= 3 & X$var3 > 11))
- ## var1 var2 var3
- ## 5 3 6 15
- ## 1 2 NA 12
X[(X$var1 <= 3 | X$var3 > 15),]
- ## var1 var2 var3
- ## 5 3 6 15
- ## 1 2 NA 12
- ## 3 1 8 11
X[which(X$var1 <= 3 | X$var3 > 15),]
- ## var1 var2 var3
- ## 5 3 6 15
- ## 1 2 NA 12
- ## 3 1 8 11
對於取子集的函式subset,在幫助文件中有一段warning是值得我們注意的:“This is a convenience function intended for use interactively. For programming it is better to use the standard subsetting functions like [, and in particular the non-standard evaluation of argument subset can have unanticipated consequences."
資料的變換
常見的資料變換函式有:
-
abs(x) 絕對值
-
sqrt(x) 開根號
-
ceiling(x) 求上線,例:ceiling(3.475) = 4
-
floor(x) 求下線,例:floor(3.475) = 3
-
round(x,digits=n) 四捨五入,例:round(3.475,digits=2) = 3.48
-
signif(x,digits=n) 四捨五入,例:signif(3.475,digits=2) = 3.5
-
cos(x), sin(x) etc.三角變換
-
log(x) 對數變換
-
log2(x), log10(x) 以2、10為底的對數變換