Rで値を標準化するときに{base}
のscale()
(つまり普通のscale()
)を使うが、データフレームを標準化するときには注意が必要となる。
当たり前だが、標準化するという目的上{base}
のscale()
は数値型しか基本的には受け付けない1。
なのでデータフレームを標準化しようとしたときに次のように数値型じゃない列(iris
データのSpecies
が因子型)を含んでいるとエラーが出る。
scale(iris)
## Error in colMeans(x, na.rm = TRUE): 'x' は数値でなければなりません
回避策としては次のように数値のところだけ抜き出して標準化するとよい(iris
データの1~4列目は数値型)。
d <- scale(iris[1:4]) # 長いのでhead()で冒頭のみ表示 head(d)
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## [1,] -0.8976739 1.01560199 -1.335752 -1.311052
## [2,] -1.1392005 -0.13153881 -1.335752 -1.311052
## [3,] -1.3807271 0.32731751 -1.392399 -1.311052
## [4,] -1.5014904 0.09788935 -1.279104 -1.311052
## [5,] -1.0184372 1.24503015 -1.335752 -1.311052
## [6,] -0.5353840 1.93331463 -1.165809 -1.048667
数値のところだけ標準化して因子型や文字列のデータを含んだデータにしたかったら、例えば次のように数値のところだけ標準化して、数値じゃないところをくっつける作業をする。
df <- cbind(scale(iris[1:4]), iris[5]) # 長いのでhead()で冒頭のみ表示 head(df)
## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1 -0.8976739 1.01560199 -1.335752 -1.311052 setosa
## 2 -1.1392005 -0.13153881 -1.335752 -1.311052 setosa
## 3 -1.3807271 0.32731751 -1.392399 -1.311052 setosa
## 4 -1.5014904 0.09788935 -1.279104 -1.311052 setosa
## 5 -1.0184372 1.24503015 -1.335752 -1.311052 setosa
## 6 -0.5353840 1.93331463 -1.165809 -1.048667 setosa
小さいデータだとそんなに手間じゃないが、少し大きデータになったり、数値型と文字列・因子型が入り乱れている場合にはちょっとめんどくさい。
こんな判定は勝手に機械でやってほしい。
というわけで、データフレームを標準化するときに数値型なら標準化して、そうでなければそのままの値のままにしておいてくれるscale()
のジェネリック関数を作った(需要がありそうなので世の中(CRAN)を探せばありそうではあるがぱっと見つけられなかった)。
scale.data.frame <- function(x, center = TRUE, scale = TRUE){ if(all(class(x) != "data.frame")){ warning("Only one data frames can be handled.") return(NA) } ans.list <- lapply(x, function(x){ if(is.numeric(as.matrix(x))) scale.default(x, center, scale) else x }) do.call(cbind.data.frame, ans.list) }
使ってみる。ジェネリック関数なので上記の関数を読み込んだら(か下にリンクを貼り付けたパッケージをlibrary()
で読み込んだら)、scale()
の標準化対象のオブジェクトのクラスがdata.frame
なら勝手にこっちで処理される。
z.iris <- scale(iris) # 長いのでhead()で冒頭のみ表示 head(z.iris)
## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1 -0.8976739 1.01560199 -1.335752 -1.311052 setosa
## 2 -1.1392005 -0.13153881 -1.335752 -1.311052 setosa
## 3 -1.3807271 0.32731751 -1.392399 -1.311052 setosa
## 4 -1.5014904 0.09788935 -1.279104 -1.311052 setosa
## 5 -1.0184372 1.24503015 -1.335752 -1.311052 setosa
## 6 -0.5353840 1.93331463 -1.165809 -1.048667 setosa
数字で入力されていても、その値が文字列だったり因子型だったらそのまま値を返すように処理するようにしている。
# サンプルデータを作る。value1は数字が因子、value2は数字が文字、value3は数字が値 sample.df <- data.frame(value1 = factor(c(1,2,1,2,3), levels = c(1:4)), value2 = as.character(c("1","2","1","2","3")), value3 = c(1, 2, 1, 2, 3)) # 型の確認。や因子型が値は1~3だけどレベルが1~4になっているのを確認する。 str(sample.df)
## 'data.frame': 5 obs. of 3 variables:
## $ value1: Factor w/ 4 levels "1","2","3","4": 1 2 1 2 3
## $ value2: chr "1" "2" "1" "2" ...
## $ value3: num 1 2 1 2 3
# データフレームをまるっと数値のみ標準化する。 z.sample.df <- scale(sample.df) # 中身を見てみる。 z.sample.df
## value1 value2 value3
## 1 1 1 -0.9561829
## 2 2 2 0.2390457
## 3 1 1 -0.9561829
## 4 2 2 0.2390457
## 5 3 3 1.4342743
# 型の確認。因子型が値は1~3だけどレベルが1~4になっているのを確認する。 str(z.sample.df)
## 'data.frame': 5 obs. of 3 variables:
## $ value1: Factor w/ 4 levels "1","2","3","4": 1 2 1 2 3
## $ value2: chr "1" "2" "1" "2" ...
## $ value3: num -0.956 0.239 -0.956 0.239 1.434
ちゃんと、文字列型や因子型のところは変換されず(型も保持される、勝手に因子型を文字列型とかにはしないし因子はレベルも保持される)、数値のところだけが標準化される。
パッケージ化した
今までブログ上で書き散らかした関数(以外もある)を少し関数名や細かい挙動を調整して、上記の関数もまとめたものをパッケージ化してGitHubからインストールできるようにした(GitHubでソースコードを管理したいだけ)。
install.packages("remotes") remotes::install_github("indenkun/infun")
でインストールできる。詳しくはないが関数の紹介はREADMEに書いたつもり。
自分で使いたい関数をパッケージ化した感じで今後も適当に自分で使いたい関数があれば適宜入れていく。自作関数であっても汎用性があるものは適宜パッケージ化したほうが名前空間的にいいし、Rスクリプトファイルの紛失が防げる。
雑感
データフレーム(数値型と因子型、文字列型が混在しているもの)の中の数値を標準化する用事(標準化する必要があったのは数値型全部ではなかったが)があって、そのときに数値型を選ぶか因子型や文字列型のところを取り除いてやるのがめんどくさいなぁと思って作った。正しいデータハンドリングとしてはちゃんとここを標準化するんだよという、ちゃんと狙って標準化するのがいい気がするが、とりあえずデータフレームを投入して数値は全部標準化しておきたいという横着したい気持ちがにじみ出ている関数である。
作ってて困ったのは、最初上で載せた関数のコードのところでlappy()
じゃなくてsapply()
を使って直接さっさとデータフレームを作ってやろうと思ったら、因子型をそのままの値で返すようにしているのになんだかんだ中であって因子型のレベルで値になってしまう自体になってしまった。
そこで打開策としてlappy()
でリストにして(リストにしておけば因子型は因子型のままレベルも保持される)、cbind()
でリストをくっつけていこうと考えた。
しかしここでもだたのcbind()
だとまたもや因子型がレベルの値に置き換わってしまうので、直接cbind.data.frame()
を指定することで回避することに成功した。
あとは、そもそも列の型判定をするときに数値型かそうでないかを判定するだけでいいのだが、データフレームで例えばiris
の1列目は数値型なのだが、
is.numeric(iris[1])
## [1] FALSE
とFALSE
になってしまうという問題に見舞われた。apply
系の関数じゃなくてfor
関数なら
is.numeric(iris[,1])
## [1] TRUE
という判定で数字を入れ替えながら回すこともできたが、なんとなくapply
系で書いたほうがかっこよさそうだと思ったので、
is.numeric(as.matrix(iris[1]))
## [1] TRUE
とas.matrix()
をかませてこの問題を回避することにした。あまりスマートな方法じゃない気がするのでなにかいい方法がわかったら置き換えるかもしれない。
あと最初にクラスを判定するコードを入れているが、S3のジェネリック関数なのでdata.fame以外が入ってくることはなさそうだが、infun:::scale.data.frame()
でクラスに関係なく強制的に呼び出されたときにたとえば「データフレームを強制的に標準化してくれるんだろー」とデータフレームをc()
で雑に2つ突っ込まれたらどういう挙動をさせるのか(この場合クラスはリストになるので受け付けないよというのでもいいが)など決めるのが面倒だったので、型判定と見せかけて、データフレーム一個しか受け付けないよというちょっとわけのわからない判定用コードにしている。
ただ、判定用のコードならclass(x) != "data.frame"
でも良さそうだと思うかもしれないが、世の中には
class(ChickWeight)
## [1] "nfnGroupedData" "nfGroupedData" "groupedData" "data.frame"
みたいにクラスいっぱい持ってる組み込みデータのデータフレームもあるのでこういうのを処理できるようにしている。
2021/04/10追記:{e1071}
のscale_data_frame()
との差異
上記のscale.data.frame()
作ってから、{e1071}
のscale_data_frame()
を見つけて「これは車輪の再発明してしまった(楽しくやれたのでヨシ(๑•̀ㅂ•́)و✧)」と思った。
で、{e1071}
のscale_data_frame()
のコードを見てみるとscale.data.frame()
とはちょっと挙動が異なる様子。
具体的な例としては次のようなデータフレームを作る。
df <- data.frame(value1 = c(T, F, T, F, T), value2 = 1:5, value3 = factor(c(1, 2, 3, 2, 1), levels = 1:10), value4 = as.character(1:5))
value1
は論理型、value2
が数値、value3
が因子型、value4
が文字列になっている数字となっている。
このとき、{e1071}
のscale_data_frame()
では次のようになる。
e1071::scale_data_frame(df)
## value1 value2 value3 value4
## 1 0.7302967 -1.2649111 1 1
## 2 -1.0954451 -0.6324555 2 2
## 3 0.7302967 0.0000000 3 3
## 4 -1.0954451 0.6324555 2 4
## 5 0.7302967 1.2649111 1 5
scale.data.frame()
では次のようになる({infun}
から:::
で強制的に呼び出す、前述しているようにいずれかの手法で関数をロードしていればscale()
で呼び出せる)。
infun:::scale.data.frame(df)
## value1 value2 value3 value4
## 1 TRUE -1.2649111 1 1
## 2 FALSE -0.6324555 2 2
## 3 TRUE 0.0000000 3 3
## 4 FALSE 0.6324555 2 4
## 5 TRUE 1.2649111 1 5
出力結果を見てもらえればすぐに分かるように、{e1071}
のscale_data_frame()
の方は論理型の方もTRUE
が1
、FALSE
が0
の数値(論理型の実態は数字の0、1なのでそれに従っている)として扱われて標準化されている。
これは意図せずこういう挙動になっているのではなく、{e1071}
のscale_data_frame()
のソースコードを読むと、標準化するかどうか判定対象を選ぶときに
i <- vapply(x, is.numeric, NA) | vapply(x, is.logical, NA)
としていて、意図的に理論型も変換できる対象に含めるようにしている。
確かに、デフォルトのscale.defalt()
は
scale.default(df$value1)
## [,1]
## [1,] 0.7302967
## [2,] -1.0954451
## [3,] 0.7302967
## [4,] -1.0954451
## [5,] 0.7302967
## attr(,"scaled:center")
## [1] 0.6
## attr(,"scaled:scale")
## [1] 0.5477226
と論理型を標準化できるけれども……。データフレームを流し込んだときに意図せず型まで変えてしまうのはやや微妙な気がするがそれは使用用途次第なのか?とりあえず、自分がやりたかったこと(scale.data.frame()
)と、{e1071}
のscale_data_frame()
でできることが違ったという話。
別に実現できることが全く一緒でも何が困るわけでもないんだけどねという話。
:追記終了
-
scale(x = iris, center = FALSE, scale = FALSE)
などと何がしたいのかわからない処理なら数値じゃない値(iris
データのSpecies
が因子型)を含んでいても受け付ける。↩