factorと数値型の変換でとりあえず区切り文字付き数字の変換方法についてめどは立ったが、そもそも区切り文字付き数字がなぜfactorになるのかがわからない。文字列にするにはパラメータを指定する必要がある。そもそも数値にただしく変換される文字形式もよくわかっていない。
そこで実際にread.csv()で何が起きているかを調べてみた。
read.csvの定義を調べてみると以下のように表示されている。
> read.csv function (file, header = TRUE, sep = ",", quote = "\"", dec = ".", fill = TRUE, comment.char = "", ...) read.table(file = file, header = header, sep = sep, quote = quote, dec = dec, fill = fill, comment.char = comment.char, ...) <bytecode: 0x0000000012be6438> <environment: namespace:utils>
read.csvの本体はread.tableであり、read.csvはパラメータを付加してread.tableを呼び出していることがわかる。
次にread.tableについてみてみる。ソースは以下から参照できる。
https://svn.r-project.org/R/trunk/src/library/utils/R/readtable.R
ファイルを読み込んでカラムに振り分ける処理はおおむね以下の手順となる。
- ファイルを開く
- EOFチェック
- 行の読み込み
- トークン
- 型設定
- 出力
- すべてのトークンについて処理
- 次の行もしくはファイルの終わり
- ファイルを閉じる
いま興味があるのは型設定だけなので、ソースからその処理だけを抜き出す。それっぽいのは以下の処理。
for (i in (1L:cols)[do]) {
data[[i]] <-
if (is.na(colClasses[i]))
type.convert(data[[i]], as.is = as.is[i], dec=dec,
numerals=numerals, na.strings = character(0L))
## as na.strings have already been converted to <NA>
else if (colClasses[i] == "factor") as.factor(data[[i]])
else if (colClasses[i] == "Date") as.Date(data[[i]])
else if (colClasses[i] == "POSIXct") as.POSIXct(data[[i]])
else methods::as(data[[i]], colClasses[i])
}
どうやら列の型が指定されていない場合には、type.convert()という関数を使っているらしい。この関数について調べてみる。type.convert()のヘルプを見てみると以下のように記述されている。
Convert a character vector to logical, integer, numeric, complex or factor as appropriate.
文字列を論理型、整数型、数値型、コンプレックス、ファクタに変換するための関数である。ではtype.convert()について検証してみる。
まず数値でtype.convert()を実行してみる。
> type.convert(10) 以下にエラー type.convert(10) : 最初の引数は文字モードでなくてはいけません
説明にもあるとおり、文字列のみ受け付けるようで、エラーとなった。次に区切り文字、小数点なしの数値文字列を変換してみる。
> type.convert("10")
[1] 10
数値変換された。クラスを確認してみる。
> class(type.convert("10") )
[1] "integer"
整数型となっている。
では、小数点付きにしてみる。
> type.convert("10.0")
[1] 10
> class(type.convert("10.0"))
[1] "numeric"
数値変換された。表示上は整数型に見えたが、型を調べてみたらnumericであった。小数点以下があると予測されるので、この変換は正しい。
では桁区切り文字付きにしてみた。
> type.convert("1,000.0")
[1] 1,000.0
Levels: 1,000.0
ファクタ型となってしまった。
この結果から、read.csv()ではtype.convert()によりデータ型変換が行われていると推測される。
type.convert()はRの内部コードとなりCで記述されている。コードは以下から参照できる。
http://svn.r-project.org/R/trunk/src/library/utils/src/io.c
コメントは以下のようになっている。
/* type.convert(char, na.strings, as.is, dec, numerals) */ /* This is a horrible hack which is used in read.table to take a character variable, if possible to convert it to a logical, integer, numeric or complex variable. If this is not possible, the result is a character string if as.is == TRUE or a factor if as.is == FALSE. */
たしかにソースを読んでみたが短時間では解析が難しいがわかる範囲で追ってみる。まずこの関数は判別した型を以下の構造体に保管している。
Typecvt_Info typeInfo; /* keep track of possible types of cvec */
typeInfo.islogical = TRUE; /* we can't rule anything out initially */
typeInfo.isinteger = TRUE;
typeInfo.isreal = TRUE;
typeInfo.iscomplex = TRUE;
またdoneローカル変数で終了チェックをしているようである。この情報を手掛かりとして処理を追ってみた。
まずislogical, isinteger, isrea, iscomplexについて処理をしている。これらの型であると判断できたらdone=TRUEにしている。
if (typeInfo.islogical) {
PROTECT(rval = allocVector(LGLSXP, len));
|
|
|
if (typeInfo.islogical) done = TRUE; else UNPROTECT(1);
}
if (!done && typeInfo.isinteger) {
PROTECT(rval = allocVector(INTSXP, len));
|
|
|
}
if(typeInfo.isinteger) done = TRUE; else UNPROTECT(1);
}
if (!done && typeInfo.isreal) {
PROTECT(rval = allocVector(REALSXP, len));
|
|
|
if(typeInfo.isreal) done = TRUE; else UNPROTECT(1);
}
if (!done && typeInfo.iscomplex) {
PROTECT(rval = allocVector(CPLXSXP, len));
|
|
|
if(typeInfo.iscomplex) done = TRUE; else UNPROTECT(1);
}
このつぎに以下の条件分岐がある。
if (!done) {
if (asIs) {
|
}
else {
as.is=FALSEでファクタになるという説明がされていたのでelseについてみてみる。処理を追ってみると以下のロジックを見つけた。ここでファクタ型の設定をしている。
setAttrib(rval, R_LevelsSymbol, levs);
PROTECT(a = mkString("factor"));
setAttrib(rval, R_ClassSymbol, a);
UNPROTECT(3);
よってtype.convertでは以下のように処理が流れている。
- 論理型、整数型、数値型、コンプレックス型に変換する
- 変換ができなければasisにより処理を分ける
- asis=falseならファクタにする
ではなぜ桁区切り文字付き数字が数字に変換されないかを見てみる。まず整数型と浮動小数点型の変換はそれぞれ以下のようになっている。
INTEGER(rval)[i] = Strtoi(tmp, 10);
if (INTEGER(rval)[i] == NA_INTEGER) {
typeInfo.isinteger = FALSE;
REAL(rval)[i] = Strtod(tmp, &endp, FALSE, &data, i_exact);
if (!isBlankString(endp)) {
typeInfo.isreal = FALSE;
変換に用いている関数はStrtoiとStrtodである。Cで簡単にテストをしてみる。
#include <stdio.h>
#include <stdlib.h>
int main(){
double x;
char s[128], *e;
char *target = "1,000";
x = strtod(target, &e);
printf("変換前数値=%s\n", target);
printf("変換後数値=%.2f\n", x);
printf("変換不可能部分=%s\n", e);
}
結果は以下のようになった。
変換前数値=1,000 変換後数値=1.00 変換不可能部分=,000 続行するには何かキーを押してください . . .
まとめるとread.csvで桁区切りが受け付けられないのはc言語のstrtoiおよびstrtodの仕様によることがわかった。
type.convertはRの内部関数であるから手を入れるのは難しい。
read.table()で以下のように2か所ほど手を入れて”f_numeric”という指定で桁区切りを受け付けるようにしてみた。
colClasses[colClasses %in% c("real", "double")] <- "numeric"
known <- colClasses %in% c("logical", "integer", "numeric", "complex",
"character", "raw", "f_numeric")
else if (colClasses[i] == "factor") as.factor(data[[i]]) else if (colClasses[i] == "Date") as.Date(data[[i]]) else if (colClasses[i] == "POSIXct") as.POSIXct(data[[i]]) else if (colClasses[i] == "f_numeric") as.numeric(gsub(pattern = ",", replacement = "", x = data[[i]], fixed = TRUE)) else methods::as(data[[i]], colClasses[i])
残念ながら、エラーが出てしまって、うまくいかない。
> revenue_01 <- read.csv(file="data.csv",head=TRUE, colClasses=c("character", "f_numeric"))
以下にエラー methods::as(data[[i]], colClasses[i]) :
no method or default for coercing “character” to “f_numeric”
時間がないのでここまで。
いろいろ経緯はあるでしょうが,桁区切りのついた数値は,他の言語でも読めない(その言語のプログラムを記述しているC言語の制約による)ので,根本的には,数値は桁区切りしない,もしされているなら,「データを用意するときに,桁区切りを外して保存する」ということでしょう。Excelを使って作業するのなら,「書式」,「セル」,で「表示形式」を「標準」にするとか,普通のエディタならば「カンマを空文字列に全置換する」など。