歩いたら休め

If the implementation is easy to explain, it may be a good idea.

【R】パイプで引数のリストを展開(Pythonのfunc(*args))してパイプで渡す方法を探しています

DBに何日分かデータが入っていなかったため、DBの中身をチェックする必要がありました。 そこで、以下の日付の範囲を取る関数を作り、DBの戻り値にないものを探すようにしました。

library(dplyr)
library(purrr)

# 日付の範囲を取る関数
date_range <- function(begin_date, end_date) {
  return(
    seq(
      as.Date(begin_date, format='%Y%m%d'),
      as.Date(end_date, format='%Y%m%d'),
      by='days'
    )
  )
}

# チェックした日付の範囲
date_range('20160101', '20160109')
# [1] "2016-01-01" "2016-01-02" "2016-01-03" "2016-01-04" "2016-01-05" "2016-01-06" "2016-01-07" "2016-01-08"
# [9] "2016-01-09"

# DBの戻り値をチェック
date_range('20160101', '20160109') %>%
  purrr::keep(~ !. %in% db_data$date) # db_dataはdateカラムに日付が入ってるデータフレームだと考えてください
# [1] "2016-01-07" 
# 7日のデータが入ってなかった!

ただし、as.Date(***, format='%Y%m%d')と2回も書いてしまっており、イケてる書き方ではありません。

そこで、日付の文字列のリストを、purrr::map関数を使ってDate型のリストに変換し、 (Pythonの引数展開のように)リストを展開して引数として与える方法は無いかと考えました。

# pythonでRの関数を使った擬似コード
from functools import partial

# 日付の文字列のリストをDate型のリスト(正確にはジェネレーターだけど)に変換する
date_scope_str = ['20160101', '20160109']
date_scope_dt = map(partial(as.Date, format='%Y%m%d'), date_scope_str)

# 引数に*を付けることで、リストを展開して与える
seq_days = partial(seq, by='days')
seq_days(*date_scope_dt)

Rでも引数を展開し、更にパイプ(%>%)で与えられると便利そうです。

というわけで、「R 引数展開」や「R argument expansion」とかで検索すると、Rでは引数の展開にdo.call関数を使うように書いてました。

blogs.yahoo.co.jp

stackoverflow.com

これを使ってdate_range関数を再実装するとこんな感じになりました。

date_range(begin_date, end_date) {
  return(
    list(begin_date, begin_date) %>% 
      purrr::map(as.Date, format='%Y%m%d') %>% 
      do.call(what = function(x, y){seq(x, y, by = 'days')})
  )
}

日付の文字列のリストをpurrrmap関数でDate型のリストに変換し、それをdo.callseq関数に渡しています。 as.Date(***, format='%Y%m%d')を1箇所にまとめることができました。 ただし、引数の順番やby = 'days'の引数の部分適用のため、what =の後にラムダ式を繋げてキモい感じになっています。

そこで、以下のような部分適用の関数を作ってみました。

# 引数展開&部分適用の関数
# purrrとかdplyrにこういう関数無いかな…
expantion <- function(args, func, ...) {
  return(
    do.call(
      function(x, y) { func(x, y, ...) },
      args
    )
  )
}

date_range <- function(begin_date, end_date) {
  return (
    list(begin_date, end_date) %>% 
      purrr::map(as.Date, format='%Y%m%d') %>% 
      expantion(seq, by='days') # スッキリ!
  )
}

date_range('20160101', '20160109')
# [1] "2016-01-01" "2016-01-02" "2016-01-03" "2016-01-04" "2016-01-05" "2016-01-06" "2016-01-07" "2016-01-08"
# [9] "2016-01-09"

ただ、これを毎回実装するのもアレだし、↑の関数だと引数が2つの場合しか対応できないし、 他人に渡すとき困るし、微妙な感じです。 purrrlift系の関数がdo.callのラッパーらしいのでこんなことができるんじゃないかと思ってるんですが、 いまいち使い方が分かりません。

notchained.hatenablog.com