cucumber flesh

Rを中心としたデータ分析・統計解析らへんの話題をしていくだけ

データフレームの変数を正しく扱うためにreadr::type_convert関数やreadr::parse関数群を使う

Rをやっていると時々、データ型の違いによる関数実行の失敗や不正確な結果の出力が起こる。errorで処理が停止したりwarningで間違っていることがわかると良いのだけど、間違ったデータ型で処理が通ってしまって、結果を見て「あれへんだな」ということがあると良くない。

(自分の中で)ありがちなのが、日付・時間型で扱いたい変数が文字列となっているような、うまくパースできていないという状況。うっかり日付・時間型のデータを入力に用いる関数に投げるとエラーになったりする。

library(tidyverse)
# 例のために文字列で「日付・時間」のデータを用意
(d <- data_frame(
  datetime = c("2017-07-12 08:30:11",
               "2017-07-12 09:10:36",
               "2017-07-12 10:43:42")
))
## # A tibble: 3 x 1
##              datetime
##                 <chr>
## 1 2017-07-12 08:30:11
## 2 2017-07-12 09:10:36
## 3 2017-07-12 10:43:42

日付・時間型として正しく扱うため、lubridate::as_datetime() (baseの関数であればas.POSIXct()か)を使えば良いのだけど、

d %>% mutate(
  datetime = lubridate::as_datetime(datetime)
)
## # A tibble: 3 x 1
##              datetime
##                <dttm>
## 1 2017-07-11 23:30:11
## 2 2017-07-12 00:10:36
## 3 2017-07-12 01:43:42

と書くのがちょっとだるい。そんな時には表題のreadrパッケージの関数type_convert()を使う。

# readrパッケージはtidyverseに含まれる
d %>% 
  type_convert()
## # A tibble: 3 x 1
##              datetime
##                <dttm>
## 1 2017-07-12 08:30:11
## 2 2017-07-12 09:10:36
## 3 2017-07-12 10:43:42

楽。

この関数は何をしているかというと、文字列として扱われているデータフレームの変数のデータ型をよしなに変換してくれるというもの(utilsにあるtype.convert()とは別物。話がそれるがこの関数の変換はあまりよろしくない気がする…)。

今回のように、本来、日付・時間や数値であるはずの変数が文字列になっている場合に使うと良いだろう。

また、readrにはベクトルを入力として、データ型をパースし直すparse_*関数群が備わっている。これはこれで便利。

d$datetime %>% parse_datetime()
## [1] "2017-07-12 08:30:11 UTC" "2017-07-12 09:10:36 UTC"
## [3] "2017-07-12 10:43:42 UTC"
d$datetime %>% parse_guess()
## [1] "2017-07-12 08:30:11 UTC" "2017-07-12 09:10:36 UTC"
## [3] "2017-07-12 10:43:42 UTC"
parse_datetime("2017年7月12日 9時20分", "%Y年%m月%d日 %H時%M分") 
## [1] "2017-07-12 09:20:00 UTC"
parse_date("2017-7-12", "%Y-%m-%d")
## [1] "2017-07-12"

readrパッケージに関しては

qiita.com

も参考までに。

Kaggleでも利用されている高圧縮ファイル形式7zをRで解凍する

たまたまKaggleのDatasetsをのぞいていたのだが、そこで.7zという見慣れない形式のファイルが提供されていた。容量はテスト用のファイルで1GBを超えている。大きい…。ダウンロードするにも時間がかかるのだけど、それよりも問題だったのは、この.7z圧縮ファイルを解凍する方法がわからないことだった(勉強不足なのでこの.7zというのを扱ったことがなかった)。

Wikipediaによれば、「7z形式ではZIP形式に比べ高圧縮のファイルが作成できる」とある。なるほど、1GBを超えるような大規模ファイルはzipにするよりもこちらの方が都合が良さそうだ。

で問題は、解凍方法。せっかくなのでRでやりたい。検索したらすぐに答えが見つかった。

stackoverflow.com

archiveパッケージを使うと良いとのと。

github.com

archiveパッケージは、libarchiveの機能を利用して多くの圧縮ファイルを解凍する関数を提供する。また解凍だけでなく、圧縮ファイルの作成も行える。CRANには登録されていないのでGitHub経由でインストールする必要がある。なお事前にlibarchiveもインストールしておく。

library(archive)

圧縮ファイルを解答する関数はarchive_extract()。圧縮ファイルのパスと解答先のディレクトリを引数に指定して実行する。すると指定したディレクトリに解凍されたファイルが保存される。

archive_extract("~/Downloads/test.7z", "~/Downloads/")

圧縮ファイルにどのようなファイルが含まれているかは、対象のファイルを引数に取り、archive()を実行することで確認できる。こんな感じ。

archive::archive("~/Downloads/test.7z")
# # A tibble: 1,532 x 3
#             path    size                date
#            <chr>   <dbl>              <dttm>
#  1         test/       0 2017-03-31 10:21:10
#  2    test/1.jpg  648369 2017-03-31 10:20:08
#  3   test/10.jpg  595322 2017-03-31 10:20:09
# ...

めでたしめでたし。

すっからかん(欠損値しかない)の列を削除して完全データにしたい

【20170518 追記】

あーだこーだしているが、ゆた兄さんに教えてもらった方法を使った方が楽チン。

追記終わり。

時として、データの特定の列が全て欠損している、ということがある。分析を進めて行く上でこのような列は不要となるため、列選択によって除外するという方法をよくとる。

それを行うためにはまず、各列の欠損値を数えて、完全データとなっている列だけを選択する必要がある。それをRでやる。パイプ処理と合わせて効率的にやる。そんな方法。

次のデータを例にする。すべてを出力しないが、変数が多く、TABLE_CATEGORYTABLE_SUB_CATEGORY1のように欠損値しかもたない列があるというのが特徴となっている。

df.list %>% ncol()
## [1] 35
df.list[, 1:6] %>% head()
## # A tibble: 6 x 5
##          `@id`                                   NAME TABLE_CATEGORY
##          <chr>                                  <chr>          <chr>
## 1 000003207760 5_国立公園の利用者数(公園、都道府県別)           <NA>
## 2 000003598444 5_国立公園の利用者数(公園、都道府県別)           <NA>
## 3 000004030655 5_国立公園の利用者数(公園、都道府県別)           <NA>
## 4 000006926450 5_国立公園の利用者数(公園、都道府県別)           <NA>
## 5 000006926494 5_国立公園の利用者数(公園、都道府県別)           <NA>
## 6 000006926518 5_国立公園の利用者数(公園、都道府県別)           <NA>
## # ... with 2 more variables: TABLE_NO <chr>, TABLE_NAME <chr>

VIM::aggr()でのデータの可視化確認。左側の棒グラフで縦軸が1.0に達しているように、特定の列は欠損値しかもたない。

df.list %>% 
  VIM::aggr(plot = TRUE,
     prop = TRUE,
     col =  "purple",
     cex.lab = 0.7,
     number = TRUE)

f:id:u_ribo:20170518070210p:plain

最初に各列の欠損値をカウントする。これにはVIMパッケージ(欠損データの可視化や補完を行う)のcountNA関数を使うか、欠損値の判定を行う関数is.na()の返り値(欠損であれば1となる)を合計する処理(sum(is.na(x)))を適用すれば良い。

df.list$TABLE_CATEGORY %>% VIM::countNA()
## [1] 8
df.list$TABLE_CATEGORY %>% is.na() %>% sum()
## [1] 8

これをデータフレームの各列に適用させる。apply()を用いても良いが、後の処理を考えてpurrrを使う。purrrmap()関数は返り値がリストであるが、map_*()を使うことで返り値をベクトルにし、データ型についても指定したできるようになる。今回は欠損値の合計を実数として取得するのでmap_int()を使う。

library(dplyr)
library(purrr)
# df.list %>% apply(2, VIM::countNA)
df.list %>% map_int(VIM::countNA)
##                        @id                       NAME 
##                          0                          0 
##             TABLE_CATEGORY                   TABLE_NO 
##                          8                          0 
##                 TABLE_NAME        TABLE_SUB_CATEGORY1 
##                          0                          8 
##        TABLE_SUB_CATEGORY2        TABLE_SUB_CATEGORY3 
##                          8                          8 
##                        URL                DESCRIPTION 
##                          0                          8 
##                     FORMAT               RELEASE_DATE 
##                          0                          5 
##         LAST_MODIFIED_DATE        RESOURCE_LICENCE_ID 
##                          8                          0 
##                   LANGUAGE                  STAT_NAME 
##                          0                          0 
##               ORGANIZATION               DATASET_NAME 
##                          0                          0 
##        TABULATION_CATEGORY   TABULATION_SUB_CATEGORY1 
##                          0                          8 
##   TABULATION_SUB_CATEGORY2   TABULATION_SUB_CATEGORY3 
##                          8                          8 
##   TABULATION_SUB_CATEGORY4   TABULATION_SUB_CATEGORY5 
##                          8                          8 
##                      CYCLE                SURVEY_DATE 
##                          0                          0 
##                  PUBLISHER              CONTACT_POINT 
##                          0                          0 
##                    CREATOR        FREQUENCY_OF_UPDATE 
##                          0                          0 
##               LANDING_PAGE                DATASET_@id 
##                          0                          0 
##        DATASET_DESCRIPTION DATASET_LAST_MODIFIED_DATE 
##                          8                          8 
##       DATASET_RELEASE_DATE 
##                          5

次に、ここから欠損を含む列と欠損していない列を区別する。これにはpurrr::keep, purrr::discardによる要素の取捨選択を適用させることで実行する。条件式として、対象(今回はデータフレームの各列の欠損値合計)が0となる要素を選択するようにする。

df.list %>% map_int(VIM::countNA) %>% 
    keep(~ .x == 0)
##                 @id                NAME            TABLE_NO 
##                   0                   0                   0 
##          TABLE_NAME                 URL              FORMAT 
##                   0                   0                   0 
## RESOURCE_LICENCE_ID            LANGUAGE           STAT_NAME 
##                   0                   0                   0 
##        ORGANIZATION        DATASET_NAME TABULATION_CATEGORY 
##                   0                   0                   0 
##               CYCLE         SURVEY_DATE           PUBLISHER 
##                   0                   0                   0 
##       CONTACT_POINT             CREATOR FREQUENCY_OF_UPDATE 
##                   0                   0                   0 
##        LANDING_PAGE         DATASET_@id 
##                   0                   0

これらの情報を用いて、列選択を行う。dplyr::one_of()は文字列ベクトルで与えた変数を選択するselect_helpers関数群の一種である。

df.list.mod <- df.list %>% 
  select(
    one_of(
      df.list %>% map_int(VIM::countNA) %>% 
    keep(~ .x == 0) %>% names()
    ))

改めて欠損数を確認しておこう。

df.list.mod %>% map_int(VIM::countNA)
##                 @id                NAME            TABLE_NO 
##                   0                   0                   0 
##          TABLE_NAME                 URL              FORMAT 
##                   0                   0                   0 
## RESOURCE_LICENCE_ID            LANGUAGE           STAT_NAME 
##                   0                   0                   0 
##        ORGANIZATION        DATASET_NAME TABULATION_CATEGORY 
##                   0                   0                   0 
##               CYCLE         SURVEY_DATE           PUBLISHER 
##                   0                   0                   0 
##       CONTACT_POINT             CREATOR FREQUENCY_OF_UPDATE 
##                   0                   0                   0 
##        LANDING_PAGE         DATASET_@id 
##                   0                   0

このような処理で完全データを得ることができる。

keep()one_of()の組み合わせは、このほかにも色々な条件に応用できそうだ。

Enjoy!