備忘ログ

チラシの裏的備忘録&メモ

Rでベクトルにas.numeric()で数値にできない文字列が含まれているかチェックする方法のメモ

追記(2021/11/04)

データフレーム中のas.numeric()で数値にできない文字列が含まれているかチェックする方法について。

下に書いているコードはベクトルで取り出した場合はうまくいくのだけれどデータフレームの場合にはうまく行かないので、対応し修正版を{infun}パッケージにfind.not.numeric.value()として入れた。

GitHub - indenkun/infun: This is a collection of R utilities functions for me, but maybe also for you.

ソースコードは上記のGitHubのページを見てもらえれば。

インストールは次のようにするとできる。

install.packages("remotes")
remotes::install_github("indenkun/infun")

具体的には、データフレームの場合に列を$で取り出したとき例えばdf$value3みたいに取り出せばベクトルとして判定されるけれど、df[3]として列を取り出すとデータフレームとして取り出されるので、後者の場合下のコードだとうまく挙動しなかったのでその点を修正した。

自分でたまに使っていて、データフレームから列を[]で取り出した時にうまく挙動しなくてあれと思ったので修正した。$で取り出すには列名が必要だけれど、ループ処理で列番号で取り出すときなんかは$で取り出すのはちょっと手間がかかるなと思ったから。

副産物として、データフレームを列を指定しないで入れたり、複数列のデータフレームを入れると、as.numeric()で数値にできない文字列が含まれている列を返すようになった。

使い方は下のものと殆ど変わらないが示しておく。

サンプルデータを次のように作る。

df <- data.frame("ID" = 1:30,
                 value_1 = rep(100, 30),
                 value_2 = c(1:20, "21*1", 22:30),
                 value_3 = c(1:10, "11*2", 12:21, "22*3", 23:30),
                 value_4 = c(1:9, NA, "1..1", "12*1", 13:20, "-2-1", -22, 23:27, 2.8, -29,  "30"))

infun::find.not.numeric.value()に入れると、1列またはベクトルの場合はその列の中でas.numeric()で数値にできず強制的にNAにされてしまう値が含まれる箇所を返し、2列以上のデータフレームを投入するとどの列にas.numeric()で数値にできず強制的にNAにされてしまう値が含まれるか入れた列のうちの何番目にあるかを返す。

データフレームから列を$を用いてベクトルで取り出すと次のように数値に変換できない値のある場所を返す。

infun::find.not.numeric.value(df$value_3)
## [1] 11 22

データフレームから列を[]を用いて取り出すとデータフレームとして取り出されるが、それでも同じように数値に変換できないあたいのある場所を返す。

# df[3]はdf$value2
infun::find.not.numeric.value(df[3])
## [1] 21

データフレームを投入すると数値に変換できない値のある列を返す。

infun::find.not.numeric.value(df)
## [1] 3 4 5

データフレームから[]を用いて複数列を取り出したときも、数値に変換できない値のある列を返すが、この場合は取り出された複数列の中で何番目か(最初のデータフレームは関係なく)を返す。

infun::find.not.numeric.value(df[2:4])
## [1] 2 3

ちょっと前に自分で使っていて使いにくいなと思ってGitHub上では直していたが、たまたま見たアクセス履歴を見たらこの記事へのgoogleからの検索が比較的多かったので修正した旨を書いてみた。

(追記終了)

find.not.numric.value()

RでCSVファイルなどからデータを読み込んだときに、数値だと思っていた列がどこかに不適切な文字列様の値があり、文字列型でデータが読み込まれてしまうことがある。

そこで、Rで任意のベクトル(数値型で読み込まれると思ったけど文字列型で読み込まれてしまった列とか)をチェックして、数値型にできない文字列が含まれている場合はその値がどこに含まれているかチェックするテキトー関数を作った。{purrr}パッケージを使用するのでインストールされていなかったらinstall.pacakges("purrr")でインストールが必要({tidyvers}をインストールしていると一括でインストールされているはず)。

find.not.numeric.value <- function(x, where = c("number", "logical")){
  where <- match.arg(where)
  
  output.df <- data.frame(do.call(rbind, purrr::map(x, purrr::quietly(as.numeric))))
  
  if(where == "number"){
    if(length(which(output.df$warnings != "character(0)")) > 0) which(output.df$warnings != "character(0)")
    else NA
  }
  else if(where == "logical") output.df$warnings != "character(0)"
}

使ってみる。サンプルデータを次のように作る。

df <- data.frame("ID" = 1:30,
                 value_1 = rep(100, 30),
                 value_2 = c(1:20, "21*1", 22:30),
                 value_3 = c(1:10, "11*2", 12:21, "22*3", 23:30),
                 value_4 = c(1:9, NA, "1..1", "12*1", 13:20, "-2-1", -22, 23:27, 2.8, -29,  "30"))

サンプルデータではIDvalue_1はすべて数値になっているが、“value_2”では21行目のデータが文字列(数値になにか注釈をつけていた*印がついている)となっており、value_3は11列目と22列目で同様に文字列になっている。value_4に至ってはぐちゃぐちゃでタイプミスと思われる“1..1”や、“-2-1”、全角の“30”が含まれている。csvファイルなどでこういうデータを読み込むとvalue_2や、value_3、当然value_4は数値ではなく文字列としてすべての値が読み込まれてしまう。

str(df)
## 'data.frame':    30 obs. of  5 variables:
##  $ ID     : int  1 2 3 4 5 6 7 8 9 10 ...
##  $ value_1: num  100 100 100 100 100 100 100 100 100 100 ...
##  $ value_2: chr  "1" "2" "3" "4" ...
##  $ value_3: chr  "1" "2" "3" "4" ...
##  $ value_4: chr  "1" "2" "3" "4" ...

前述したfind.not.numeric.value()を使うと

find.not.numeric.value(df$value_2)
## [1] 21
find.not.numeric.value(df$value_3)
## [1] 11 22
find.not.numeric.value(df$value_4)
## [1] 11 12 21 30

とどこ(何番目の値)に数値じゃない文字列(または全角の数字)が含まれているのかがわかる。一応理論型で数字のところをFALSE、文字列のところをTRUEで返すようにもできる。

find.not.numeric.value(df$value_2, where = "logical")
##  [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
## [13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE
## [25] FALSE FALSE FALSE FALSE FALSE FALSE
find.not.numeric.value(df$value_3, where = "logical")
##  [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE
## [13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE
## [25] FALSE FALSE FALSE FALSE FALSE FALSE
find.not.numeric.value(df$value_4, where = "logical")
##  [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE
## [13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE
## [25] FALSE FALSE FALSE FALSE FALSE  TRUE

これは文字列は一定の値で置換するなどの処理のときに使えるかもしれない。

動機

Rでエクセルのファイルなどからデータを読み込んこんだときに、たまに数値の列のはずが文字列として読み込まれてしまっていることがある。その主たる原因は、タイプミスや注釈用の文字列が値にくっついていることに起因するものである可能性がある。

こういう文字列が入力されている値や、タプミスしている値は不規則に存在することが多いので、パターンで探すよりもエクセル上で目視上パチパチ直していくのが多分早いと思う。しかし、ここでの修正もどこが文字列・タイプミスなのかがすぐに見つけられればいいが、複数箇所に渡り文字列・タイプミスがあったりすると修正忘れがあったりして、エクセルで直してRで読み直して文字列で再度読み込まれてしまいまたエクセルで直しに行かなければいけない、というちょっと面倒なことが起こる。

これが意外とストレスになってくるので、とりあえず機械的に数値型に変換できない値を文字列とみなして探し出す関数を作った。データフレームの形をいじったり、する花のある(?)前処理じゃないけど、こういう面倒くさい前処理は楽したい。

ということで関数化した。

基本的には値だけど、文字列が混じってしまったベクトルを「値なんじゃー」といって、as.numeric()で強制的に数値型に変換すると、つぎのように数値型に変換できなかった文字列はNAに変換されてしまう上に、どこが文字列だったのかわからない。

as.numeric(df$value_2)
## Warning: 強制変換により NA が生成されました

##  [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 NA 22 23 24 25
## [26] 26 27 28 29 30

value_2のように欠損値がない場合はNAに変換された場所が文字列だったんだなと思うのだけれど、問題はvalue_4のように欠損値があってかつ文字列がある場合で、

as.numeric(df$value_4)
## Warnings: 強制変換により NA が生成されました

##  [1]   1.0   2.0   3.0   4.0   5.0   6.0   7.0   8.0   9.0    NA    NA    NA
## [13]  13.0  14.0  15.0  16.0  17.0  18.0  19.0  20.0    NA -22.0  23.0  24.0
## [25]  25.0  26.0  27.0   2.8 -29.0    NA

NAだけではどこがもともと文字列だったのかわからないという展開になってしまう。

そこでfind.not.numeric.value()を使うと、

find.not.numeric.value(df$value_4)
## [1] 11 12 21 30

ああ、何番めの値が変な値になってるんだなぁー、とひと目でとわかる。変な値の場所が分かればあとはそこを目視するとよいので、

df$value_4[11]
## [1] "1..1"

としたりして変な値を確認したり、エクセルでファイル開いてみてみたりしてお直しするとよいと思う。

少し幸せになれるかもしれない。

参考・雑感

warnig messageを拾うために、{purrr}quietly()を使っている。

r - How can I check whether a function call results in a warning? - Stack Overflow

でこの関数を初めて知った。すごい便利。

as.numeric()で強制変換したときに出るwarnig messageを拾うためにmessage系の関数の仕様やmessageを拾う方法を調べたが日本語圏内ではちょっとわからなくて、stackoverflowで調べたらすぐに上記のページが出てきて解決した。

こういうデータを見るたびに、前述したように不規則な箇所の修正になることが多いので結局エクセルなどで手作業でデータを直すのだけれど複数箇所で直す場所があったときに直したつもりでCSVファイルを読み込んで直し忘れに気づくのは結構ストレスで、なんとかしたいなぁと思っていた。

あんまり日本語で調べたときにこの手のトラブルを回避する手法見つけられなかったのでメモしておく。もしかしたらもっと良い教科書的な手法があるのかもしれない。

蛇足

与えた値にある文字が含まれているかどうかを調べる、{base}系ならgrep()grepl()関数や{tidyvers}のパッケージ群の{stringr}で似たような挙動をするstr_locate()str_detect()があり、数字だけでできている値を見分けるならこっちらでもできる。しかし、CSVファイルなどとして読み込んだときに数値型となるか、つまりas.numeric()で(強制変換せずに)数値型にできる値かどうかを調べるにはかなり骨が折れそうで、とりあえず自分は達成できなかった。

具体的に{base}grep()の例で書いていく、他の関数も似たような挙動で動く(意図した挙動までは至っていないが)はず。

とりあえず、今回のサンプルデータのvalue_2value_3であれば自然数しか無いので簡単に鑑別できる。

grep(pattern = "[^[:digit:]]", df$value_3)
## [1] 11 22

きれいな不規則な形であればこれでもいいが、問題はごちゃごちゃしているvaleu_4のようなもの。

grep(pattern = "[^[:digit:]]", df$value_4)
## [1] 11 12 21 22 28 29

マイナスや小数点がパターンに含まれないので含んでみる。あとは全角が[:digit:]でマッチしてしまうのでperl = TRUEとしてみる。

grep(pattern = "[^[:digit:].-]", df$value_4, perl = TRUE)
## [1] 12 30

“1..1”や“-2-1”が拾えなくなってしまう。

必ずマイナスは先頭1個だけある、小数点は必ず1個まで数字と数字の間に挟まれている……など条件を加えればなんとかなるかもしれないが、ちょっと大変そう。

いろいろ工夫してみているが、今の所うまく書けていない。

蛇足(追記)

[http://goldenstate.cocolog-nifty.com/blog/2018/10/r-1552.html:title]

で提案されている例では、文字列をひとつずつ入力するのには有効かもしれない。NAなのか自分でわかっている場合は自分で取り除けばいい。あるいは欠損値が含まれていない場合にも有効だと思う(今回のダミーデータでいうとvalue_2value_3の場合)。

ただ、NAを含むベクトルについて調べようと思ったとき(今回のダミーデータでいうとvalue_4の場合)に、もとからNAだった値も、文字列に変換できなくてNAになる値も同じ用に扱っているのでちょっと意図した挙動ではなかった。

欠損値があってもなくてもfind.not.numeric.value()の関数の場合は使えると思っている。

(追記終了)

2021/04/07追記:パッケージ化した

今までブログ上で書き散らかした関数(以外もある)を少し関数名や細かい挙動を調整して、上記の関数もまとめたものをパッケージ化してGitHubからインストールできるようにした(GitHubソースコードを管理したいだけ)。

GitHub - indenkun/infun: This is a collection of R utilities functions for me, but maybe also for you.

install.packages("remotes")
remotes::install_github("indenkun/infun")

でインストールできる。詳しくはないが関数の紹介はREADMEに書いたつもり。

自分で使いたい関数をパッケージ化した感じで今後も適当に自分で使いたい関数があれば適宜入れていく。自作関数であっても汎用性があるものは適宜パッケージ化したほうが名前空間的にいいし、Rスクリプトファイルの紛失が防げる。

:追記終了