Rでデータのバリデーションチェックをやってみる

# はじめに
業務でデータのバリデーションチェックを行う機会がありました.
本番では別のソフトを使いましたが，Rでも実行できるし，
使いようによってはもっと簡単に実行できるのでは？と思い
Rでバリデーションチェックを行ってみたので方法を記事にしました．

# バリデーションチェックとは
データマネジメントの現場では，しばしばデータの「品質チェック」
（バリデーションチェック）が行われます．
大量のデータをすべて目で見るのは大変で，間違いも起こってしまいます．
そこで，今あるデータの「品質」を簡単に評価するために，データの範囲や
極端な値をチェックし異常をあらかじめ浮き彫りにするという操作です．

# Rでバリデーションチェックを行う
Rには，統計解析に用いる関数群はもちろんですが，データ操作のための関数や
パッケージも充実しています．
ファイルの出力も容易なので，再現性のある結果を共有することもできます． 

## 実行環境
今回の実行環境です．
Windows10 64bit 言語：日本語

```{r}
R.Version()$version.string
```

```{r:Console}
[1] "R version 3.6.2 (2019-12-12)"
```

データの読み込みには以下のパッケージを使います

```{r} 
 library(readr)  #csvファイルの読み込み
 library(readxl) #xlsxファイルの読み込み
```
バリデーションチェックやデータクレンジングにおいては
**どのような処理を行ったか記録を残しておくこと**は重要です．
なので，データを読み込む前に，R上での欠損値と空白（スペース）の扱いを抑えておきます．

## スペースの扱い

次のようなデータを用意します．

|スペースの位置|半角スペース  |全角スペース  |
|---|--:|--:|
|前     |" ab"  |　ab  |
|後ろ   |"ab "  |ab　  |
|前後   |" ab " |　ab　  |
|文字中 |"a b"  |a　b  |

※半角スペースの方は，みやすいように`""`でかこっています

各行は文字前後の空白の位置を表しています．
それぞれ文字の前，後ろ，前後，文字中に
半角と全角のスペースが挿入されています．

このデータが`sample`という名称で，保存されているとします．

### csvの場合
データがcsv形式で保存されている場合
`readr`パッケージの`read_csv`関数で読み込みます．

```{r}
Data<-read_csv("sample.csv")
```
データを確認します．

```{r}
Data
```

```{r:Console}
# A tibble: 4 x 3
  スペースの位置 半角スペース 全角スペース
  <chr>          <chr>        <chr>       
1 前             ab           　ab        
2 後ろ           ab           ab　        
3 前後           ac           　ac　      
4 文字中         a b          a　b   
```

ここでデータを見てみると，半角スペースの方は
文字前後のスペースが削除されていることがわかります．

実は`read_csv`関数は，引数`trim_ws`でデータ前後の空白を
データ読み込み時に自動で削除するか選択することができます．

このオプションはデフォルトでは`TRUE`になっているため
普段意識する必要がありませんが，
データクレンジングを行う際は，暗黙のうちにクレンジング処理が
行われていることを認識しておく必要があります．

また，`trim_ws=FALSE`として読み込めば
データを「そのまま」の状態で読み込みことができます．

```{r}
Data<-read_csv("sample.csv",trim_ws = F)
Data
```

```{r:Console}
# A tibble: 4 x 3
  スペースの位置 半角スペース 全角スペース
  <chr>          <chr>        <chr>       
1 前             " ab"        　ab        
2 後ろ           "ab "        ab　        
3 前後           " ac "       　ac　      
4 文字中         "a b"        a　b   
```

### xlsxの場合
データがxlsxで保存されている場合も，使用する関数は異なりますが処理は同様です．

```{r}
Data<-read_xlsx("sample.xlsx")
Data
```
```{r:Console}
# A tibble: 4 x 3
  スペースの位置 半角スペース 全角スペース
  <chr>          <chr>        <chr>       
1 前             ab           　ab        
2 後ろ           ab           ab　        
3 前後           ac           　ac　      
4 文字中         a b          a　b   
```

```{r}
Data<-read_xlsx("sample.xlsx",trim_ws = F)
Data
```
```{r:Console}
# A tibble: 4 x 3
  スペースの位置 半角スペース 全角スペース
  <chr>          <chr>        <chr>       
1 前             " ab"        　ab        
2 後ろ           "ab "        ab　        
3 前後           " ac "       　ac　      
4 文字中         "a b"        a　b  
```

## 欠損値の扱い

Rではデータが欠損している場合，`NA(Not Available)`として読み込まれます．

以下のようなデータを用意します．

|スペースの数|半角スペース  |全角スペース  |
|---|--:|--:|
|1     |" "  |"　"  |
|2   |"  "  |"　　"|
|3   |"   " |"　　　"　  |
※今回も，みやすいように`""`でかこっています

`sample`という名前で保存し，データを読み込んでみます．

```{r}
Data<-read_csv("sample.csv")
Data
```

```{r:Console}
# A tibble: 3 x 3
  スペースの数 半角スペース 全角スペース
         <dbl> <lgl>        <chr>       
1            1 NA           　          
2            2 NA           　　        
3            3 NA           　　　  
```

半角スペースはその数に関わらず，欠損値として扱われ
全角スペースはそのまま文字列として扱われていることがわかります．

`xlsx`形式でも同様です．

```{r}
Data<-read_xlsx("sample.xlsx")
Data
```
```{r:Console}
# A tibble: 3 x 3
  スペースの数 半角スペース 全角スペース
         <dbl> <lgl>        <chr>       
1            1 NA           　          
2            2 NA           　　        
3            3 NA           　　　  
```

これらのことからデータの読み込み時には

 * 全角スペースは文字と同様の扱いがなされる
 * 半角スペースのみは欠損，文字前後の半角スペースは自動でトリミングの対象となる
 
と認識しておけば問題なさそうです．

# バリデーションチェックをやってみる

実際にバリデーションチェックを行います．
バリデーションチェックでは次のような項目をチェックします．

- データ型
- データの長さ
- ユニーク数
- 欠損値の数
- NULL値の数
- 空白スペースの有無

もし，対象データに**データ定義書**があればそれに基づいて
値の長さや範囲の逸脱がないかチェックを行います．(ない場合もしばしば...)

今回は簡単にサンプルとして次のようなデータが**100件**あるとします．
(データは自作です.必要であれば[ここ](https://github.com/y-yasutomo/validation-check)からダウンロードできます．)

```{r:データの読み込み}
Df<-read_csv("sample_data.csv")
```


**カラムの定義**

|ID| CATALOG_ID  |NUMBER|
|---|--:|--:|
|CHAR(4)   |CHAR(3)  |INT  |

**データの概観**
上位5列

```{r}
head(Df,n=5)
```

```{r:Console}
# A tibble: 5 x 3
  ID    CATALOG_ID NUMBER
  <chr> <chr>      <chr> 
1 A284  BJF        548   
2 A476  RGP        701   
3 A582  EAU        344   
4 A591  MJO        780   
5 111   KIA        943  
```

データの定義からは，データの型と文字数しかわからないので
ここからデータを見て，推測していくことになります．

ID列は，英文字+数字3桁の文字列
CATALOG_ID列はアルファベット3文字からなる文字列
NUMBER列には文字通り整数が入っていそうです．

しかし，すでにID列の5列目にあるようにデータの定義からもれた
値が存在しています．

データの数が数万を超えてくると，当然一つ一つチェックするのは難しいので
これらの異常をバリデーションチェックにより明らかにしましょう．

### チェック用の関数を定義

先ほどの項目を調べるための関数をいくつか定義します．
それぞれデータの最小/最大値，最長/最短値，欠損値の数を取り出します．

```{r}
min_val<-function(x)min(x,na.rm = T)
max_val<-function(x)max(x,na.rm = T)
max_len<-function(x)max(nchar(x),na.rm = T)
min_len<-function(x)min(nchar(x),na.rm = T)
num_unique<-function(x)length(unique(x))
num_na<-function(x)sum(is.na(x))
```
```{r:varidation_check.R}
 validation_check<-function(Df){
#結果を格納するオブジェクトを用意
 res_validation<-
   data.frame(matrix(NA,ncol(Df),6,
                     dimnames = list(colnames(Df),
                                     c("val_min","val_max","len_min","len_max",
                                       "unique_num","na_num"))))
#関数を適用
 res_validation$val_min<-apply(Df,2,min_val)
 res_validation$val_max<-apply(Df,2,max_val)
 res_validation$len_min<-apply(Df,2,min_len)
 res_validation$len_max<-apply(Df,2,max_len)
 res_validation$unique_num<-apply(Df,2,num_unique)
 res_validation$na_num<-apply(Df,2,num_na)
 res_validation
 }
```


### バリデーションチェック
```{r}
validation_check(Df)
```
しかし，ここで何やらエラーメッセージが．

```{r:Console}
Error in nchar(x) : invalid multibyte string, element 25
```
データの25列目が悪さをしているようです．
中身を見てみます．

```{r}
Df[25,]
```
```{r:Console}
# A tibble: 1 x 3
  ID            CATALOG_ID NUMBER
  <chr>         <chr>      <chr> 
1 "\x88\x9f345" VTY        479   
```
元データも確認します．

|ID| CATALOG_ID  |NUMBER|
|---|--:|--:|
| 亜345|  VTY|  479|   

文字化けが起きているようです．
Windows環境の場合，`Shift-JIS`か`CP932`でエンコード
していることが原因です．

元データを変更するわけにはいかないので，Rで対処します．
データ読み込み時に，エンコードを明示的に指定することで回避できます．

```{r}
Df<-read_csv("sample_data.csv",locale = locale(encoding = "CP932"))
Df[25,]
```
```{r:Console}
# A tibble: 1 x 3
  ID    CATALOG_ID NUMBER
  <chr> <chr>      <chr> 
1 亜345 VTY        479   
```

文字化けは回避できました．
再度バリデーションチェックを行います．

```{r}
validation_check(Df)
```
```{r:Console}
           val_min val_max len_min len_max unique_num na_num
ID              　   亜345       1       4        100      1
CATALOG_ID     ARO     YVI       3       3         99      0
NUMBER         100     a12       3       3         95      0
```
チェックができました．
ここでデータの定義を再確認します．

**カラムの定義**

|ID| CATALOG_ID  |NUMBER|
|---|--:|--:|
|CHAR(4)   |CHAR(3)  |INT  |

結果からは以下の不整合がみられます．

* ID列
    - 長さが4以外の値が存在する
    - 入力ミスと思われる値が存在する(`亜345`)
    - 全角スペースが存在する

* CATALOG_ID列
    - ユニーク数が99となっているが，これはすべてユニークでなくても問題ないのか．

* NUMBER列
    - 数値以外の値が存在する

簡単なチェックでも，これらの不整合が確認できます．
不明点や気になる点はデータのお客様に確認を行います．

# まとめ
Rでデータのバリデーションチェックを行いました．
データを読み込み時の注意点とバリデーションチェックの確認観点を整理します．

### 注意点

Rでデータを読み込む際は，暗黙のうちに半角スペース削除のクレンジング処理が
行われているということを認識しておく必要があります．

また，Windows環境の場合，データにうまく扱えない文字が存在する恐れがあるので
エンコードには注意を払う必要があります．

### バリデーションチェック

- 基本的にはデータ定義書の範囲からの逸脱がないか調べる
- 極端な値の確認（最大/最小,最長/最短，桁数）
- 主キーの候補を考えてみる
- 数値の最大値(最小値)も疑ってみる

不明点はデータの所有者に確認し，解決するようにしましょう．

# おわりに

ご意見や誤りなどあればコメントをお願いします．

# 補足

今回のバリデーションチェックでは，**全角スペース**も文字列として扱っています．
全角スペースも欠損値として扱うことも多いと思います．その場合

- 全角スペースも`NA`に置き換える
- スペースを除いた文字の最小/最大のチェックを行う

などもう少し丁寧な処理が必要となります．

# 参考文献

- [Rを日本語Windowsで使うときにアクセント付きのローマ字を扱う方法](https://id.fnshr.info/2015/05/03/r-windows-accent/)
- [Rで計量政治学入門](https://shohei-doi.github.io/quant_polisci/index.html)