Rの細かいTipsまとめ(小さいTipsの寄せ集め)


独立した記事にはならないが、それぞれ便利かつ重要な小さなRのTipsを紹介。

Rで文字列をコマンドとして実行する(eval)

Rの繰り返し処理などでコマンドの一部のみを書き換えて同様の処理を行いたい、たとえば対象のデータフレームのみ変えて同じ処理を実行したい場合がある。

そういう場合には動的な部分(データフレームオブジェクト名)を変数として含むコマンドの文字列を生成し、その文字列をコマンドとして実行することになる。

言葉にするとややこしいが、他の言語でもあるeval処理である。

コマンドが1行の場合

最も単純なケースである。

基本形

ファイル名に動的部分を含むテキストファイル(activity_type1.txt)を読み込み、名前に動的部分を含むテーブル(t_activity_type1)に格納する例

.f <- 'activity_type1'
eval(parse(text=paste0("t_", .f, " <- fread('/path/to/datadir/", .f, ".txt', encoding='UTF-8', sep='\t', na.strings='')")))

.fが動的部分で、ファイル名である。

そして動的部分を

.f <- 'activity_type2'

などと変えても同様に実行できる。

eval(parse(text=paste0("固定文字列", 動的部分, "固定文字列")))

が基本になる。paste0()関数がパーツを結合して文字列を生成する関数で、それ以外はおまじないだと思っておけばいい。コマンドの固定文字列部分にクォーテーションマーク'を含む場合、文字列全体はダブルクォーテーションマーク"で囲む。

コマンド部分のみ文字列変数として切り出してソースを見やすくする

この記法だとソースコードの中で動的部分が見えにくく、コマンドの一部を書き換えたり繰り返し利用したりするのに向かない(特にカッコの数がわからなくなる)ので、コマンド部分を変数として取り出したのが以下になる。

.f <- 'activity_type1'
.str_cmd <- paste0("t_", .f, " <- fread('/path/to/datadir/", .f, ".csv', encoding='UTF-8', sep='\t', na.strings='')")
eval(parse(text=.str_cmd))

.str_cmdがコマンドの文字列になる。こうすると

.f <- 'activity_type1'
.str_cmd <- paste0("t_", .f, " <- fread('/path/to/datadir/", .f, ".csv', encoding='UTF-8', sep='\t', na.strings='')")
eval(parse(text=.str_cmd))
.str_cmd <- paste0("t_", .f, " %>% mutate_at(ends_with('_date'), as.Date)")
eval(parse(text=.str_cmd))

のように動的コマンドを複数生成して実行しやすくなる。

複数行のコマンドを実行する場合

上記の方法だと繰り返しeval(parse(text=.str_cmd))が必要になってしまう。動的に実行するコマンドが複数行の場合でも毎回必要になり、あまりイケてない。

そこで一時ファイルを生成して、一時ファイルに対して処理を行う。

.f <- 'activity_type1'
.str_cmd <- paste0(".tmp <- fread('/path/to/datadir/", .f, ".csv', encoding='UTF-8', sep='\t', na.strings='')")
eval(parse(text=.str_cmd)) # .tmpという一時ファイルに代入
# .tmpに対する処理
.tmp %>% 
  mutate_at(vars(ends_with('_date')), as.Date) %>% 
  mutate_at(vars(ends_with('_type')), as.factor) %>% 
  mutate_if(is.logical, as.integer) -> .tmp
# .tmpの内容を個別の永続テーブルに入れる
.str_cmd <- paste0(".tmp -> t_", .f); eval(parse(text=.str_cmd))

.tmpに対する処理の部分が動的ではない普通のコマンドとなり、ソースコードの可読性も上がり、再利用やメンテナンスもしやすくなった。

シンプルなコマンド1行であれば動的にする必要もあまりないが、複数行のコマンドを繰り返し実行したい場合にはこの方法が力を発揮する。

forループの中で実行する

以上の例を複数のファイルに対して繰り返し実行する。
動的な部分をベクトルに代入し、forループの中に先の処理を入れる。

# 動的な部分の定義
filenames <- c('activity_type1', 'activity_type7', 'activity_click', 'event_unsubscribe', 'event_old')
# 繰り返し
for (.f in filenames) {
  # いったんdata.tableオブジェクトとして取り込む
  .str_cmd <- paste0("t_", .f, " <- fread('/path/to/datadir/", .f, ".csv', encoding='UTF-8', sep='\t', na.strings='')")
  eval(parse(text=.str_cmd))
  # 作業用オブジェクトを生成
  eval(parse(text=paste(".tmp <- t_", .f, sep="")))
  .tmp %>% 
    mutate_at(vars(ends_with('_date')), as.Date) %>% 
    mutate_at(vars(ends_with('_type')), as.factor) %>% 
    mutate_if(is.logical, as.integer) -> .tmp
  .tmp[mail_type %like% '購入お礼', c('mail_type', 'mail_type2', 'mail_type3', 'channel'):=data.table('SAF', '購入お礼', NA, NA)]
  # 作業用オブジェクトから元のオブジェクトに戻す
  eval(parse(text=paste0(".tmp -> t_", .f)))
}

ソースコードがかなり洗練される。

# 条件に合致したオブジェクトに対して一括処理
for (.i in ls(pattern='t1_.*_r_')) {
  .str_cmd <- paste('class(', .i, ') <- "matrix"', sep='')
  eval(parse(text=.str_cmd))
}

Rのdata.table形式で日付データを扱う際はPOSIXltにすること

Rのdata.tableで何気なく日付データを扱っていたらキレイにハマったのでまとめておく。

まずデータフレームで数字列を日時に変換する。

> head(x.df)
        date
1   20151122
2   20151118
3   20151123
4   20150330
5   20151106
6   20150801

このようなデータに対して、以下の3通りの方法で日時データに変換する。

  • strptime()のみ実行
  • strptime()してからas.POSIXlt()
  • strptime()してからas.POSIXct()
> x.df$date2 <- strptime(as.character(x.df$date), format="%Y%m%d")
> x.df$date3 <- as.POSIXlt(strptime(as.character(x.df$date), format="%Y%m%d"))
> x.df$date4 <- as.POSIXct(strptime(as.character(x.df$date), format="%Y%m%d"))
> 
> head(x.df)
      date      date2      date3      date4
1 20151122 2015-11-22 2015-11-22 2015-11-22
2 20151118 2015-11-18 2015-11-18 2015-11-18
3 20151123 2015-11-23 2015-11-23 2015-11-23
4 20150330 2015-03-30 2015-03-30 2015-03-30
5 20151106 2015-11-06 2015-11-06 2015-11-06
6 20150801 2015-08-01 2015-08-01 2015-08-01

strptime()はPOSIXltを返すのでdate2とdate3は同じだが比較のため。
POSIXltもPOSIXctも日時データだが、POSIXltでは内部で日時をリスト型変数として、POSIXctは数値型変数として格納している。

これに対してdata.tableでは

> head(x.dt)
         date
  1: 20151122
  2: 20151118
  3: 20151123
  4: 20150330
  5: 20151106
  6: 20150801
> x.dt %>% mutate(date2 = strptime(as.character(date), format="%Y%m%d"), date3 = as.POSIXlt(strptime(as.character(date), format="%Y%m%d")), date4 = as.POSIXct(strptime(as.character(date), format="%Y%m%d"))) -> x.dt
 警告メッセージ: 
1:  `[.data.table`(`_dt`, , `:=`(date2, strptime(as.character(date),  で: 
  Supplied 11 items to be assigned to 344 items of column 'date2' (recycled leaving remainder of 3 items).
2:  `[.data.table`(`_dt`, , `:=`(date3, as.POSIXlt(strptime(as.character(date),  で: 
  Supplied 11 items to be assigned to 344 items of column 'date3' (recycled leaving remainder of 3 items).
> 
> head(x.dt)
       date                    date2                    date3      date4
1: 20151122             0,0,0,0,0,0,             0,0,0,0,0,0, 2015-11-22
2: 20151118             0,0,0,0,0,0,             0,0,0,0,0,0, 2015-11-18
3: 20151123             0,0,0,0,0,0,             0,0,0,0,0,0, 2015-11-23
4: 20150330       22,18,23,30, 6, 1,       22,18,23,30, 6, 1, 2015-03-30
5: 20151106       10,10,10, 2,10, 7,       10,10,10, 2,10, 7, 2015-11-06
6: 20150801 115,115,115,115,115,115, 115,115,115,115,115,115, 2015-08-01

変な警告とともにPOSIXlt形式をそのままリストとして扱ってしまう。
POSIXlt形式は内部ではリストなのだが、日付としてのラッパーが外れてしまうのである。

ということでdata.table内ではPOSIXltは扱えない。どちらでもいいと思っていた諸君、POSIXctに限定しよう。

Rで郵便番号マスタを作成する(データフレーム、data.table両対応)

分析用のデータの中には地域名がほしい形(都道府県名、市区町村名で区切られた形)で含まれておらず、一方で郵便番号が含まれていることもある。

その場合郵便番号から地域名を取得することになるが、その都度そのコードを書くのは面倒。
しかも郵便番号が更新されることもあるので、常に最新版を持ってくるようにしたい。

そこでコピペでそのまま郵便番号マスタを生成できるコードを用意した。
日本郵便の郵便番号データから最新版のCSVを取得してきて、郵便番号マスタを生成するものである。
これがあればRの中で常に最新版の郵便番号マスタを生成できる。
分析用データと結合して地域名を付けることも簡単である。

日本郵便の郵便番号データから最新版のCSVを取得する。
CSVファイルに含まれるカラムと形式の詳細は以下参照。

http://www.post.japanpost.jp/zipcode/dl/readme.html

必要なカラムのみ取得すればいい。
ここでは郵便番号(2列目)と市区町村コード(1列目)、都道府県名(7列目)、市区町村名(8列目)を取得する。なお郵便番号の形式はハイフンを含まない7ケタの数字列。

データフレームで読み込む場合

require(curl)
temp <- tempfile()
download.file("http://www.post.japanpost.jp/zipcode/dl/kogaki/zip/ken_all.zip", temp)
files <- unzip(temp)
df.zipcode <- read.csv(files[1], na.strings="", stringsAsFactors = FALSE, header=F, skip=0, fileEncoding = "shift-jis", colClasses = c("character", "NULL", "character", "NULL", "NULL", "NULL", "character", "character", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL"))
unlink(files)
unlink(temp)
rm(files, temp)
colnames(df.zipcode) <- c("citycode","zipcode","prefecture","city")

ちなみにdata.tableを使う場合はこんな感じ。
CSVファイルのエンコーディングがShift-JISでfread()が対応していないため、
一度read.csv()でデータフレームとして読み込んでからdata.tableに変換する。

data.tableで読み込む場合

require(curl)
require(data.table)
require(dplyr)
temp <- tempfile()
download.file("http://www.post.japanpost.jp/zipcode/dl/kogaki/zip/ken_all.zip", temp)
files <- unzip(temp)
read.csv(files[1], na.strings="", stringsAsFactors = FALSE, header=F, skip=0, fileEncoding = "shift-jis", colClasses = c("character", "NULL", "character", "NULL", "NULL", "NULL", "character", "character", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL", "NULL")) %>% as.data.table -> dt.zipcode
unlink(files)
unlink(temp)
rm(files, temp)
setnames(dt.zipcode, "V1", "citycode")
setnames(dt.zipcode, "V3", "zipcode")
setnames(dt.zipcode, "V7", "prefecture")
setnames(dt.zipcode, "V8", "city")

こんなふうにすれば郵便番号を含むデータから都道府県、市区町村名も紐付けできる。
ちなみに元データの郵便番号の形式がハイフンを含む999-9999形式の場合

require(stringr)
dt.data<- dt.data %>% 
mutate(zipcode = str_replace(zipcode, "([0-9]{3})-([0-9]{4})", "\\1\\2")) %>% 
left_join(dt.zipcode)

モデルの選択

anova(単純なモデル, 複雑なモデル)

p-valueを見る

圧縮ファイルの読み込み

fread("gzip -dc df.txt")

パッケージを保持してアップグレード

https://www.r-bloggers.com/how-to-upgrade-r-without-losing-your-packages/

  1. Before you upgrade, build a temp file with all of your old packages.
tmp <- installed.packages()
installedpkgs <- as.vector(tmp[is.na(tmp[,"Priority"]), 1])
save(installedpkgs, file="installed_old.rda")
  1. Install the new version of R and let it do it’s thing.

  2. Once you’ve got the new version up and running, reload the saved packages and re-install them from CRAN.

tmp <- installed.packages()
installedpkgs.new <- as.vector(tmp[is.na(tmp[,"Priority"]), 1])
missing <- setdiff(installedpkgs, installedpkgs.new)
install.packages(missing)
update.packages()

If you had any packages from BioConductor, you will need to reload those as well.

chooseBioCmirror()
biocLite() 
load("installed_old.rda")
tmp <- installed.packages()
installedpkgs.new <- as.vector(tmp[is.na(tmp[,"Priority"]), 1])
missing <- setdiff(installedpkgs, installedpkgs.new)
for (i in 1:length(missing)) biocLite(missing[i])

R関連記事