まとまったページがない?じゃあ,作ろうじゃないですか。協力宜しく。早いもの勝ち。
こんなデータで,こんなデータクリーニングしたいなあというのがあれば,具体的に書けば,それを実現する関数なりが書き込まれるだろう(かな???)というページ。
データ入力が終わったら,次にやることはデータクリーニングである。いきなりデータ解析を始めてはいけない。そもそも,入力されたデータがちゃんとしたものである保証はない。コーディングミスやタイプミスがある可能性が高いし,アンケート調査の場合には調査票のデータ自体が間違っていたり矛盾したデータであったりする。
データクリーニングとは,データのいろいろな誤りを修正することである。そのためには,誤入力の可能性があるデータを拾い出すことが必要である。これをデータチェックという。データの種類や誤りの種類によって,誤りを検出する方法もさまざまである。既存の関数を単独で使う場合もあるし,組み合わせて使えばさらに効果的なデータチェックをすることができることもある。場合によっては,専用の関数を書かなければならないこともある。
いろいろな入力ミスを検出し,その入力ミスのあるデータがどれであるかを出力し,正しいデータに置き換える。
たとえば,性別を 1(男),2(女) で入力しているのに,NA のほかに 9 とか 1.2 とか "male"という入力がある。以下の用にすれば,そういう変なデータを含む行が出力される。その中の ID No. に基づいて,元のデータがどうなっていたかを確認するのである。
> d <- data.frame(ID=1:10, sex=c(1, 2, NA, 1.2, 9, "male", 2, 1, 2, 2)) > summary(d) ID sex Min. : 1.00 1 :2 1st Qu.: 3.25 1.2 :1 Median : 5.50 2 :4 Mean : 5.50 9 :1 3rd Qu.: 7.75 male:1 Max. :10.00 NA's:1 > table(d$sex, useNA="ifany") 1 1.2 2 9 male <NA> 2 1 4 1 1 1 > d <- na.omit(d) > d[d$sex == 9,] ID sex 5 5 9 > d[d$sex == 1.2,] ID sex 4 4 1.2 > d[d$sex == "male", ] ID sex 6 6 male
変数ごとに欠損値の有無,実際に記録されているデータ値の種類をみる。カテゴリー変数だと table 関数を使うとよい。連続変数だといきなり table 関数を使うは不適切なので,cut 関数でカテゴリー化してから table 関数を使うとよい。summary 関数によれば,NA の個数,最小値,最大値(カテゴリー変数の場合には主な値の個数)などが得られる。このような関数を使うことにより,たとえば身長として235(センチ)などと誤入力されたものを見つけることができる。ただし,身長が 168 センチの人のデータを 186 センチと誤入力したようなものは見つけることはできない。
身長が 108 センチの人のデータを 196 センチと誤入力したようなものでも,別の変数たとえば年齢とクロス集計する,または散布図を描けば誤入力を発見できる場合もある。たとえば,その人の年齢が 10 歳であった場合,そんなに大きな子供は滅多にいないので,年齢の方が間違えているのかもわからないが,とにかくデータを確かめるきっかけにはなるだろう。
同じデータを2つのファイルに2度入力し,内容を比較することにより入力ミスをチェックする。 [#y26ce2ed]
compare <- function(file.a, file.b) { a <- readLines(file.a) b <- readLines(file.b) length.a <- length(a) length.b <- length(b) if (length.a != length.b) { stop(paste("ファイルの行数が違います", length.a, "行と", length.b, "行")) } max.length <- max(nchar(c(a, b))) ruler1 <- paste(sprintf("%10i", 1:(max.length%/%10+1)), collapse="") ruler2 <- paste(rep("1234567890", (max.length%/%10+1)), collapse="") for (i in 1:length.a) { string.a <- a[i] string.b <- b[i] if (string.a != string.b) { character.a <- unlist(strsplit(string.a, "")) length.character.a <- length(character.a) character.b <- unlist(strsplit(string.b, "")) length.character.b <- length(character.b) min.c <- min(length.character.a, length.character.b) character.d <- ifelse(character.a[1:min.c] == character.b[1:min.c], " ", "v") character.d <- c(character.d, rep("v", max.length-min.c)) string.d <- paste(character.d, collapse="") cat(sprintf(" %s\n %s\n %s\n%4i: %s\n %s\n\n", ruler1, ruler2, string.d, i, string.a, string.b)) } } }
使用例
> set.seed(12) # テストデータを作る > file.a <- "testdata1.dat" > file.b <- "testdata2.dat" > d <- matrix(sample(0:9, 250, replace=TRUE), 10) > write(apply(d, 1, paste, collapse=""), file=file.a) > d[sample(250, 5)] <- sample(0:9, 5) > write(apply(d, 1, paste, collapse=""), file=file.b) > compare(file.a, file.b) # ファイルの比較 1 2 3 123456789012345678901234567890 v 3: 9308949611478951141654247 9308949611488951141654247 1 2 3 123456789012345678901234567890 v v 6: 0429091924756856515266137 0429091924756856515266933 1 2 3 123456789012345678901234567890 v 8: 6518057783166680942903649 6558057783166680942903649 1 2 3 123456789012345678901234567890 v 9: 0643163133167844418159180 1643163133167844418159180 > unlink(c(file.a, file.b))
diffコマンドを使う。オプションなし
> system(paste("diff", file.a, file.b)) 3c3 < 9308949611478951141654247 --- > 9308949611488951141654247 6c6 < 0429091924756856515266137 --- > 0429091924756856515266933 8,9c8,9 < 6518057783166680942903649 < 0643163133167844418159180 --- > 6558057783166680942903649 > 1643163133167844418159180
左右に並べるyオプションをつける
> system(paste("diff -y -W60", file.a, file.b)) 0322537356476822586397504 0322537356476822586397504 8878534781828287522820110 8878534781828287522820110 9308949611478951141654247 | 9308949611488951141654247 2378135323809170372086533 2378135323809170372086533 1226852375608033488296246 1226852375608033488296246 0429091924756856515266137 | 0429091924756856515266933 1456351707945846825421760 1456351707945846825421760 6518057783166680942903649 | 6558057783166680942903649 0643163133167844418159180 | 1643163133167844418159180 0163787813495542673221586 0163787813495542673221586
単純に同一かどうかだけを判定したいときはcmpコマンドを使う
> system(paste("cmp", file.a, file.b)) testdata1.dat testdata2.dat 異なります: バイト 64、行 3
> ( d <- data.frame(a=c(2, 2, 5, 4, 1, 2, 2), + b=c("f", "r", "o", "u", "p", "r", "z"), + c=c(2, 4, 3, 5, 2, 4, 4)) ) a b c 1 2 f 2 2 2 r 4 3 5 o 3 4 4 u 5 5 1 p 2 6 2 r 4 7 2 z 4 > ( u <- unique(d) ) a b c 1 2 f 2 2 2 r 4 3 5 o 3 4 4 u 5 5 1 p 2 7 2 z 4
unique2 <- function(d, # データフレーム ID) # 基準となるデータのある列番号 { # ID が同じデータは,最後のものが残る nrow <- nrow(d) ok <- rep(TRUE, nrow) for (i in 1 : (nrow-1)) { if (sum(d[(i+1) : nrow, ID] %in% d[i, ID])) { ok[i] <- FALSE } } d[ok,] }
実行例
> ( d <- data.frame(ID=c(8, 9, 8, 9, 5, 2, 4, 6, 8, 10, 1, 2), + x=c(9, 1, 4, 5, 10, 3, 2, 12, 6, 11, 8, 7)) ) ID x 1 8 9 2 9 1 3 8 4 4 9 5 5 5 10 6 2 3 7 4 2 8 6 12 9 8 6 10 10 11 11 1 8 12 2 7 > ( d2 <- unique2(d, 1) ) ID x 4 9 5 5 5 10 7 4 2 8 6 12 9 8 6 10 10 11 11 1 8 12 2 7
質問 重複データを見てから個別に削除するということもよく行われると思うのですが、どうすればよいでしょうか。
重複するデータを表示し,どれを削除するか確認する。
unique3 <- function(d, # データフレーム ID) # 基準となるデータのある列番号 { tbl <- table(d[ID]) tbl <- tbl[tbl > 1] suf <- as.integer(names(tbl)) print(lapply(suf, function(s) d[d[,ID] == s,])) }
実行例
> unique3(d, 1) [[1]] ID x 6 2 3 12 2 7 [[2]] ID x 1 8 9 3 8 4 9 8 6 [[3]] ID x 2 9 1 4 9 5
左端に示されているのが行番号である。6, 1, 9, 2 行目を削除したいなら。
d2 <- d[-c(6, 1, 9, 2), ]
などとすればよい。
> ( d2 <- d[-c(6, 1, 9, 2),] ) ID x 3 8 4 4 9 5 5 5 10 7 4 2 8 6 12 10 10 11 11 1 8 12 2 7
zooパッケージを使って欠損値を補間する
Hmiscパッケージのimpute, fit.mult.impute関数:
欠損値の前後のデータの平均値で補完する
> x <- c(NA, 1, 2, 4, 3, NA, 5, 7, NA, 10, NA) > invisible(sapply(which(is.na(x)), + function(p) if (p != 1 && p != length(x)) x[p] <<- mean(x[p+c(-1,1)]))) > x [1] NA 1.0 2.0 4.0 3.0 4.0 5.0 7.0 8.5 10.0 NA
> x <- c(NA, 1, 2, 4, 3, NA, 5, 7, NA, 10, NA) > x[is.na(x)] <- mean(x, na.rm=TRUE) > x [1] 4.571429 1.000000 2.000000 4.000000 3.000000 4.571429 5.000000 7.000000 4.571429 [10] 10.000000 4.571429
> x <- c(NA, 1, 2, 4, 3, NA, 5, 7, NA, 10, NA) > x[is.na(x)] <- 0 > x [1] 0 1 2 4 3 0 5 7 0 10 0
平均値±k・標準偏差 の範囲以外にあるデータ値およびそのデータを持つのはどのデータかを返す関数。
hazureti <- function(x, k=2) { result <- NULL result$caselist <- x >= mean(x)+k*sd(x) | x <= mean(x)-k*sd(x) result$abnormal.values <- sort(x[result$caselist]) result }
使用例
> hazureti(iris$Sepal.Width) $caselist [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE TRUE [17] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE [33] TRUE TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE [49] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE [65] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE [81] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE [97] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE [113] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE [129] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE [145] FALSE FALSE FALSE FALSE FALSE FALSE $abnormal.values [1] 2.0 4.0 4.1 4.2 4.4
Mahalanobis <- function(dat, threshold) { dat <- na.omit(dat) n <- nrow(dat) d2 <- apply(t(t(dat)-colMeans(dat)), 1, function(z) z %*% solve(var(dat)*(n-1)/n) %*% z) dat$P <- pchisq(d2, ncol(dat), lower.tail=FALSE) dat[dat$P < threshold, ] }
使用例
> Mahalanobis(iris[1:50, 1:4], 0.05) Sepal.Length Sepal.Width Petal.Length Petal.Width P 15 5.8 4.0 1.2 0.2 0.03376522 23 4.6 3.6 1.0 0.2 0.02369321 25 4.8 3.4 1.9 0.2 0.04133150 42 4.5 2.3 1.3 0.3 0.01352573 44 5.0 3.5 1.6 0.6 0.01363081
質問:性別を表す変数で,データファイルに "male", "female" と書いてある。普通,male=1, female=2 になると思ったのに,male=2, female=1 になってしまい,理解できない。
回答:facot(d$sex, levels=c("male", "female")) のようにするとよいでしょう。
> ( d <- data.frame(sex=c("male", "female", "male", "male", "female")) ) sex 1 male 2 female 3 male 4 male 5 female > as.integer(d$sex) [1] 2 1 2 2 1 > d$sex <- factor(d$sex, levels=c("male", "female")) > d sex 1 male 2 female 3 male 4 male 5 female > as.integer(d$sex) [1] 1 2 1 1 2
こんなデータファイルを作ったとする。
x y 1 42.47 Apr 2 47.428 Aug 3 52.969 Dec 4 59.304 Jan 5 57.534 Jun 6 50.29l Mar 7 65.535 May 8 33.175 Nov 9 45.198 Oct 10 57.151 Sep
読み込んでみる。
> d <- read.table("test.dat", header=TRUE)
平均値を取ろうとして,怒られる。
> mean(d$x) [1] NA 警告メッセージ: In mean.default(d$x) : 引数は数値でも論理値でもありません。NA 値を返します
クラスを調べてみて,あらびっくり。数値データなのに,factor だといわれてしまった。
> class(d$x) [1] "factor"
こういうとき,原因は「データの中に数を表す文字以外の文字が入っている」。1文字でも紛れ込んでいると全部が factor になる。当然,平均値なんか計算できないというわけ。
たくさんあるデータのどこがイケナイのか,以下のようにして調べる。
> z <- sapply(d$x, function(y) as.numeric(as.character(y))) 警告メッセージ: In FUN(X[[10L]], ...) : 強制変換により NA が生成されました > which(is.na(z)) [1] 6 > d$x[6] [1] 50.29l 10 Levels: 33.175 42.47 45.198 47.428 50.29l 52.969 ... 65.535
よく見てみよう。1 を l(小文字のエル) と入力してしまっている。1 と l は打ち間違えないとは思うが,0 と o は近くにあるし間違えやすいかも。
以下のようなエラーメッセージが出る。
> d <- read.table("test.dat", header=TRUE) 以下にエラー scan(file, what, nmax, sep, dec, quote, skip, nlines, na.strings, : '2' 行目には,2 個の要素がありません
そういうときには,fill=TRUE を指定して,読み込んでみよう。
> d <- read.table("test.dat", header=TRUE, fill=TRUE)
データを表示してみると,いろいろな風にオカシイことがある。以下の例では,2行目のデータの区切りが「全角空白」なのが原因。
x y 1 42.47 Apr 2 47.428 Aug 3 52.969 Dec
以下のようなエラーメッセージが出る。
> d <- read.table("test.dat", header=TRUE) 以下にエラー scan(file, what, nmax, sep, dec, quote, skip, nlines, na.strings, : '1' 行目には,3 個の要素がありません
上と同じように,fill=TRUE を指定して,読み込んでみよう。
> d <- read.table("test.dat", header=TRUE, fill=TRUE)
表示してみたら,以下のようだった。本来の数値データが,rownames になってしまった。
x y 42.47 Apr 47.428 Aug test 52.969 Dec
以下のようにすればよい。
> x <- readLines("test.dat") > y <- sapply(x, function(y) length(unlist(strsplit(y, " ")))) > ( z <- unname(y) ) [1] 2 2 3 2 2 2 2 2 2 2 2
個数の違うのが何行目なのかは以下のようにすれば分かる。
> which(z == 3) [1] 3
信用残データというのがある。
csv ファイルのようなので,以下のようにしてダウンロードしてファイルとして保存してから,いざ解析のために読み込もうとしたときに,OS によってはエラーメッセージが出ることがある。
> d <- read.csv("s20080502.csv") 以下にエラー make.names(col.names, unique = TRUE) : <8b>ɗmに不正なマルチバイト文字があります
つまり,このファイルのエンコーディングが合わないのだ。
このファイルは WIndows で作られたようで,fileEncoding="cp932" を指定しないと Mac では読めない。読み込むためには,以下のようにする。ヘッダーがあると思ったけど,ないので,header=FALSE とする。
d <- read.csv("s20080502.csv", header=FALSE, fileEncoding="cp932")
うむ。なんとか読み込めたようだ。
以後の扱いに不便なので,変数(列)に名前を付けてやろう。
colnames(d) <- c("名称", "売残", "買残", "日付", "市場", "コード")
先頭のデータを見てみる。
> head(d) 名称 売残 買残 日付 市場 コード 1 極洋 499 1506 2008/05/02 1 1301 2 日水 2950.3 1086.8 2008/05/02 1 1332 3 マルニチH 4062 5067 2008/05/02 1 1334 4 サカタタネ 323.4 72.2 2008/05/02 1 1377 5 ホクト 195.9 26.8 2008/05/02 1 1379 6 住友炭 17481.5 37852 2008/05/02 1 1503
一番最初にすることは,サンプルサイズがいくつかを確認すること。
> nrow(d) [1] 2272
2272件のデータが入っている。
名称とコードによってデータ識別ができるようだが,重複データがあるかどうか調べておく。
> length(table(d$名称)) [1] 2272 > length(table(d$コード)) [1] 2272
となり,行数と等しいので重複データはないようだ。
次に,どんなデータが入っているのか,まずは,summary 関数で確認する。
> summary(d) 名称 売残 買残 日付 市場 コード 300投信: 1 -- : 530 -- : 20 2008/05/02:2272 Min. :1.000 Min. :1301 7&iHD: 1 3 : 11 2 : 16 1st Qu.:1.000 1st Qu.:4280 7シーズH: 1 16 : 8 1 : 15 Median :1.000 Median :6677 A&AM : 1 2 : 8 8 : 12 Mean :1.208 Mean :6222 A&D : 1 5 : 8 11 : 10 3rd Qu.:1.000 3rd Qu.:8238 ABCマト: 1 9 : 8 3 : 10 Max. :2.000 Max. :9997 (Other) :2266 (Other):1699 (Other):2189
むむ,前の方にも書いたけど,残高とか買残は数値データのはずだけど,"--" というデータが入力されているのが分かる。これが,欠損値なんだなあ。これがあるから,売残,買残は数値データではなく factor になってしまっている。だから,平均値なんかがとれなくなっている。
> mean(d$売残) [1] NA 警告メッセージ: In mean.default(d$売残) : 引数は数値でも論理値でもありません。NA 値を返します
"--" を NA に置き換えると同時に,元々数値だったものを数値に戻さないといけない。以下のような,簡便法では,警告メッセージがでるが,無視して良い(数値にできないものが NA になるということだから)。
> head( d$売残 <- as.numeric(as.character(d$売残)), 50) [1] 499.000 2950.300 4062.000 323.400 195.900 17481.500 74.000 6846.000 4.312 [10] 286.000 225.500 30.000 0.740 105.700 2010.540 444.000 1178.500 88.500 [19] 191.400 19.500 17.400 3088.000 3480.000 1981.000 12676.000 13932.000 13.000 [28] 10367.000 4305.400 207.000 299.000 70.000 253.000 1610.000 1369.800 47.000 [37] 1097.000 166.000 20.500 2181.000 NA 35.000 872.000 34.000 204.000 [46] 285.100 2473.000 1044.000 27.500 644.000 警告メッセージ: In head(d$売残 <- as.numeric(as.character(d$売残)), 50) : 強制変換により NA が生成されました > d$買残 <- as.numeric(as.character(d$買残))
気持ちが悪いという場合には,na.string="--" を指定してもう一度読み直すとよい(ラベル付けなども再度)。
> d <- read.csv("s20080502.csv", header=FALSE, fileEncoding="cp932", na.string="--") > colnames(d) <- c("名称", "売残", "買残", "日付", "市場", "コード")
これで,売残,買残の統計処理ができるようになった。na.rm=TRUE を忘れないように。
> mean(d$売残, na.rm=TRUE) [1] 793.7595 > mean(d$買残, na.rm=TRUE) [1] 1092.553
日付は "2008/05/02" 一種だけのようだ。 市場は 1, 2 の二種のようだ。
> table(d$市場) 1 2 1799 473
市場により,売残の平均値に差があるかどうか(意味がないかも知れないが)検定することもできる。
> t.test(d$売残~d$市場) Welch Two Sample t-test data: d$売残 by d$市場 t = 4.4822, df = 145.155, p-value = 1.488e-05 alternative hypothesis: true difference in means is not equal to 0 95 percent confidence interval: 362.5556 934.4865 sample estimates: mean in group 1 mean in group 2 830.6157 182.0947
まず,あなたの使っているマシンで,以下のようなテストプログラムを走らせてみよう。
n <- 10000 p <- 500 gc(); gc() system.time(m <- round(matrix(rnorm(n*p), n, p)*10+50)) system.time(d <- as.data.frame(m)) system.time(write.csv(d, "datacleaning.csv", quote=FALSE, row.names=FALSE)) system.time(csv <- read.csv("datacleaning.csv"))
実行例
> n <- 10000 > p <- 500 > gc(); gc() 略 > system.time(m <- round(matrix(rnorm(n*p), n, p)*10+50)) ユーザ システム 経過 a e i # n×p 行列の作成にかかる秒数 > system.time(d <- as.data.frame(m)) ユーザ システム 経過 b f j # データフレームに変換する秒数 > system.time(write.csv(d, "datacleaning.csv", quote=FALSE, row.names=FALSE)) ユーザ システム 経過 c g k # ファイルに書き出す秒数 > system.time(csv <- read.csv("datacleaning.csv")) ユーザ システム 経過 d h l # ファイルから読み出す秒数
実際にあなたが扱うデータについて,「x=行数*列数/5e6」を計算する。上で得られた秒数 a 〜 l に x を掛ければ,それぞれに必要なおよその秒数がわかる。
例えば,x = 行数*列数/5e6 = 100000*1000/5e6 = 20 であるとする。d = 2.776 であるなら,実際には約 20*2.776 = 55.52 秒かかる(実際やってみると 58 秒かかった)。
ただし,私の環境では x > 60 位になると「ユーザ」時間はだいたいそれくらいですむが,「経過」時間は予測より遙かに長くなるので注意。私の環境では,x=30のときの予測「ユーザ」時間は167程度で実測が174秒であるが,「経過」時間の予測は169であるが,実際には倍以上の378秒であった)。
いきなり,大規模なデータを扱うのはチョットマッタ。
まず,みなさんご自分の環境で,どの程度のデータ量ならばどれくらいの時間が必要かを把握しておく方がよいだろう。
入出力というのは,数値計算などに比べると数桁違いの時間が必要なのだから。
45990