内閣府が公表している「国民の祝日」というcsvファイルがひどいという話を聞いたのでRパッケージを使って現状を確認し、問題点をあげながら、処理しやすい形に整形してみようと思います。使うのは**{tidyverse}
**パッケージです。全ての機能をここで紹介することはできませんが、このパッケージ(に含まれているデータ操作のためのパッケージ)を使うとデータの前処理に必要な多くの作業を済ませることが可能になります。
pandasさんに先を越されてしまいましたが、このようなデータ処理はRでも得意な分野です
最初にこのファイルの問題点と最終的な整形結果を示しておきましょう。
- エンコード
- データ以外の文字列
- ユニークに扱えない変数名
- 文字列を日付として扱う
- 項目と値がセットになっていない
## # A tibble: 48 × 2
## name date
## <chr> <date>
## 1 元日 2016-01-01
## 2 成人の日 2016-01-11
## 3 建国記念の日 2016-02-11
## 4 春分の日 2016-03-20
## 5 昭和の日 2016-04-29
## 6 憲法記念日 2016-05-03
## 7 みどりの日 2016-05-04
## 8 こどもの日 2016-05-05
## 9 海の日 2016-07-18
## 10 山の日 2016-08-11
## # ... with 38 more rows
まず必要なパッケージを読み込みますが、**{tidyverse}
**パッケージを読み込むと今回必要なパッケージ群が一度にロードされます。
library(tidyverse)
csvの読み込み
問題1: ファイルのエンコード
内閣府のウェブサイトで公開されているcsvファイルをRに読み込ませましょう。ここでは**{readr}
**のread_csv()
という関数を使います。read_csv()
の第一引数にURLを直接指定します。
df.holiday <- read_csv("http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv")
# Error in make.names(x) :
# invalid multibyte string at '<95><bd><90><ac><32>8<94>N<81>i2016<94>N<81>j'
# In addition: Warning message:
# Missing column names filled in: 'X2' [2], 'X4' [4], 'X6' [6]
いきなりエラーになってしまいました。 read_csv()
はデフォルトのファイルエンコードとしてUTF-8を想定しています。このファイルはどうやらUTF-8で保存されたものではないらしいです。read_csv()
ではファイルエンコードの値を引数locale
を使って指定できるので、それで対処しましょう。
解決1: ファイルエンコードをShift-JISに
では、このファイルは一体どのエンコード体型を採用しているのでしょうか。それを調べる必要があります。guess_encoding()
を使えば簡単にファイルのエンコード体型を判定できます。
guess_encoding("http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv")
## encoding confidence
## 1 Shift_JIS 1
Shift_JIS
! この値をエンコードに指定しましょう。
df.holiday <- read_csv("http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv",
locale = locale(encoding = "Shift_JIS"))
## Warning: Missing column names filled in: 'X2' [2], 'X4' [4], 'X6' [6]
## Parsed with column specification:
## cols(
## `平成28年(2016年)` = col_character(),
## X2 = col_character(),
## `平成29年(2017年)` = col_character(),
## X4 = col_character(),
## `平成30年(2018年)` = col_character(),
## X6 = col_character()
## )
読み込みには成功しましたが何やら警告が表示されました。内容は、元のcsvでは1行目が
| 平成28年(2016年) | | 平成29年(2017年) | | 平成30年(2018年) | |
と第一行目に空白を含んでいるため、**{readr}
**が自動的にカラム名を与えたというものでした。これは次の問題として扱うことにします。
問題2: データ以外の文字列
読み込んだデータの先頭を確認します。このファイルでは実際は2行目がカラム名、3行目からデータが保存されていることがわかります。
df.holiday %>% head(3)
## # A tibble: 3 × 6
## `平成28年(2016年)` X2 `平成29年(2017年)` X4
## <chr> <chr> <chr> <chr>
## 1 名称 月日 名称 月日
## 2 元日 2016/1/1 元日 2017/1/1
## 3 成人の日 2016/1/11 成人の日 2017/1/9
## # ... with 2 more variables: `平成30年(2018年)` <chr>, X6 <chr>
また同様に末尾も表示してみると、最後の2行は空白行とデータとは関係のないメモ書きが含まれています。こうした余分な情報は取り除いておくことにしましょう。
df.holiday %>% tail(3)
## # A tibble: 3 × 6
## `平成28年(2016年)`
## <chr>
## 1 天皇誕生日
## 2 <NA>
## 3 月日は表示するアプリケーションによって形式が異なる場合があります。
## # ... with 5 more variables: X2 <chr>, `平成29年(2017年)` <chr>,
## # X4 <chr>, `平成30年(2018年)` <chr>, X6 <chr>
解決2: 読み込む行の指定
# 先に読み込んだcsvの行数は19行
nrow(df.holiday)
## [1] 19
余分な先頭行と末尾をそれぞれ読み込みから除外する指定をし、再度csvファイルを読み込みます。
df.holiday <- read_csv("http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv",
locale = locale(encoding = "cp932"),
skip = 1,
n_max = 16)
## Warning: Duplicated column names deduplicated: '名称' => '名称_1' [3], '月
## 日' => '月日_1' [4], '名称' => '名称_2' [5], '月日' => '月日_2' [6]
## Parsed with column specification:
## cols(
## 名称 = col_character(),
## 月日 = col_character(),
## 名称_1 = col_character(),
## 月日_1 = col_character(),
## 名称_2 = col_character(),
## 月日_2 = col_character()
## )
問題3: 変数名がユニークではない
必要なデータだけを取り出すことに成功しましたが、今度は変数名に重複があることが問題となっています。このcsvでは、2016年から2018年までの祝日の名称と日付がセットになって横方向に記録されており、祝日と日付がそれぞれ「名称」、「月日」という変数になっています。**{readr}
**では重複のある変数名に対して、数値をつけることでユニークに扱えるようにしてくれていますが、修正しておくことにましょう。
df.holiday %>% head(3)
## # A tibble: 3 × 6
## 名称 月日 名称_1 月日_1 名称_2 月日_2
## <chr> <chr> <chr> <chr> <chr> <chr>
## 1 元日 2016/1/1 元日 2017/1/1 元日 2018/1/1
## 2 成人の日 2016/1/11 成人の日 2017/1/9 成人の日 2018/1/8
## 3 建国記念の日 2016/2/11 建国記念の日 2017/2/11 建国記念の日 2018/2/11
解決3: 変数名を指定する
df.holiday <- df.holiday %>% set_names(paste(c("name", "date"), rep(2016:2018, each = 2), sep = "_"))
df.holiday %>% names()
## [1] "name_2016" "date_2016" "name_2017" "date_2017" "name_2018" "date_2018"
問題4: 日付を文字列として扱う
(これは**{readr}
**側の指定の問題なので元のcsvでは適切な処理がなされていると考えても良いかもしれません)
このファイルでは日付が%Y/%m/%d
形式で書かれていますが、読み込んだ日付の列が文字列として扱われています。これは**{readr}
**パッケージの日付のパースが%AD
を想定しているためですので、再度日付データであることを指定して読み込ませましょう。日付データのパースを指定するコードを追加し、これまでの処理をもう一度実行します。
# 日付となるべき値が文字列になっている
df.holiday$date_2016[1]
## [1] "2016/1/1"
df.holiday$date_2016[1] %>% class()
## [1] "character"
df.holiday <- read_csv("http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv",
locale = locale(encoding = "cp932",
date_format = "%Y/%m/%d"),
skip = 1,
n_max = 16) %>%
set_names(paste(c("name", "date"), rep(2016:2018, each = 2), sep = "_"))
## Parsed with column specification:
## cols(
## 名称 = col_character(),
## 月日 = col_date(format = ""),
## 名称_1 = col_character(),
## 月日_1 = col_date(format = ""),
## 名称_2 = col_character(),
## 月日_2 = col_date(format = "")
## )
# 日付データとして扱えるようになった
df.holiday$date_2016[1] %>% class()
## [1] "Date"
データの形を整える
問題5: 項目と値がセットになっていない
先に述べたように、このファイルは3年間の祝日の名称と日付の組み合わせが横方向、つまり全体で6列の値として保存されています。これでは処理に手間がかかるので、項目と値、今回では祝日の名称と各年での日付のセットとして扱えるようにします。
解決5: 項目と値のセットに整形する
df.holiday.tidy <- df.holiday %>%
gather(key = year, value = name, contains("name")) %>%
select(name, num_range("date_", 2016:2018))
df.holiday.tidy %>% head(3)
## # A tibble: 3 × 4
## name date_2016 date_2017 date_2018
## <chr> <date> <date> <date>
## 1 元日 2016-01-01 2017-01-01 2018-01-01
## 2 成人の日 2016-01-11 2017-01-09 2018-01-08
## 3 建国記念の日 2016-02-11 2017-02-11 2018-02-11
tidydata
祝日の名称と日付を正しく読み込み、扱いやすい形式にしましたが、まだこの段階でも完全なtidy dataとは言えません。というのも、例えば2017年の体育の日を抽出したい、という時にはどうすれば良いでしょうか。name列に対して「体育の日」を条件抽出すれば良いのですが、それでは他の年のデータも取得してしまいます。そこでこのデータを完全な項目と値のセットからなるtidy dataにしておきましょう。
先ほど同様、gather()
を使って複数の変数をまとめます。また、日付データ操作のために新たに**{lubridate}
**を読み込んでおきます。
library(lubridate)
df.holiday.tidy <- df.holiday %>%
gather(key = name, value = date, contains("date")) %>%
select(name = name_2016, date) %>%
# 年の列を追加します
mutate(year = year(date))
df.holiday.tidy %>%
filter(name == "体育の日", year == 2017)
## # A tibble: 1 × 3
## name date year
## <chr> <date> <dbl>
## 1 体育の日 2017-10-09 2017
tidy dataにしておくことで、 次のような処理(祝日の判定)も可能になります。祝日の一覧データなので全て祝日になるのは当然ですが、**{Nippon}
**の祝日判定関数is.jholiday
を使って判定させましょう。
library(Nippon)
res <- df.holiday.tidy %>%
mutate(holiday = is.jholiday(date))
testthat::expect_true(unique(res$holiday))
偽りの祝日はないようです。