dplyrを使いこなす!Window関数編

  • 76
    Like
  • 0
    Comment
More than 1 year has passed since last update.

はじめに

dplyrの使い方にちょっと慣れてくると、「あー、これもうちょっと簡単にできないの?」みたいな事が出てきたりします。

今回は、そんな悩みをほんのちょっと解決できるかもしれない、Window関数について解説したいと思います。
SQLに詳しい人はすぐイメージできると思いますが、私の周りにもWindow関数の存在自体を知らない人が結構居たのでいい機会なので、ざっくりまとめます。

dplyrってなんぞやという方は、基礎編の記事を見ていただければと。

Window関数を使うと簡単にできることの例

とは言っても、具体的に何ができるのか、分からなかったら読むのもメンドクサイので、まずは簡単にできることを紹介します。

  • ランキング(タイ順位考慮あり、なし等含む)
  • 前日比、前週比(前後のレコードとの比較等)
  • 累積(累積和等)
  • 移動平均(Windowサイズの指定、Windowの位置、重み等)

どれも自分でやれなくはないけど、結構メンドクサイですよね。。
でも、Window関数を使えるようになると、dplyrの一連の処理の流れの中に数行で組み込めるので、簡単だし、コードも見やすくなり、そして、何より、仕事が捗ってる感を得ることができます。(初回限定)

集約関数とWindow関数の違い

まずは、いつも使っている集約関数とWindow関数の違いについて、整理します。

集約関数

集約関数は、複数の入力レコードに対し、1つのレコードを出力します。
count、sum、mean、max、min等がその代表です。

Window関数

対して、Window関数は、複数の入力レコードに対し、レコード毎に1つのレコードを出力します。
ランキングや累積和をイメージすると、分かりやすいかと思います。

という感じなので、集約関数の場合は、group_byして、summarise内で使う場合がほとんどだと思いますが、
Window関数は、新しい列を追加するケースが多いので、mutate内で使用することになります。
(慣れてくれば、filter内で使うことも増えてくるかもしれません。)

それでは、具体的に見ていきましょう。

ランキング関数

最初は、比較的イメージしやすいランキング関数から。
ランキング系の関数には、以下の6つの関数があります。

主なランキング関数

関数 説明
row_number 昇順にランキングを付ける。同じ値がある場合は、最初に来た方を優先
min_rank 昇順にランキングを付ける。同じ値がある場合は、同じ順位を付ける。gapあり
dense_rank 昇順にランキングを付ける。同じ値がある場合は、同じ順位を付ける。gapなし
percent_rank min_rankを0~1にリスケールしたもの
cume_dist 累積割合。percent_rankの累積和ではない
ntile n個の群に分割する

ランキング関数構文

構文
> row_number
function (x) 

> min_rank
function (x) 

> dense_rank
function (x) 

> percent_rank
function (x) 

> cume_dist
function (x) 

> ntile
function (x, n) 

例1:ランキング関数(昇順)

R
data.frame(x = c(5 , 1 , 3 , 2 , 2 , NA)) %>%
   dplyr::mutate(row_number=row_number(x)) %>%
   dplyr::mutate(min_rank=min_rank(x)) %>%
   dplyr::mutate(dense_rank=dense_rank(x)) %>%
   dplyr::mutate(percent_rank=percent_rank(x)) %>%
   dplyr::mutate(cume_dist=cume_dist(x)) %>%
   dplyr::mutate(ntile=ntile(x , 3))

   x row_number min_rank dense_rank percent_rank cume_dist ntile
1  5          5        5          4         1.00       1.0     3
2  1          1        1          1         0.00       0.2     1
3  3          4        4          3         0.75       0.8     2
4  2          2        2          2         0.25       0.6     1
5  2          3        2          2         0.25       0.6     2
6 NA         NA       NA         NA           NA        NA    NA

min_rank(gapあり)、denserank(gapなし)は、タイ順位があった場合にその次の順位を飛ばすか、飛ばさないかの差です。
上記例で言えば、1,2,2,4となるか、1,2,2,3となるかの違いですね。

cume_distは、上位5%とか出したいときや、全体の90%を占めるラインは何処かを求める際に使うイメージです。
percent_rankの累積ではないので、勘違いしないようにしましょう。

入力値がNAの場合は、どの関数もランキングせずに、NAを返します。

例2:ランキング関数(降順)

R
# 降順
# スコアの高い順でランキングしたい場合等は、desc関数を使用すればよい
> data.frame(x = c(5 , 1 , 3 , 2 , 2 , NA)) %>%
   dplyr::mutate(row_number=row_number(desc(x))) %>%
   dplyr::mutate(min_rank=min_rank(desc(x))) %>%
   dplyr::mutate(dense_rank=dense_rank(desc(x))) %>%
   dplyr::mutate(percent_rank=percent_rank(desc(x))) %>%
   dplyr::mutate(cume_dist=cume_dist(desc(x))) %>%
   dplyr::mutate(ntile=ntile(desc(x) , 3))

   x row_number min_rank dense_rank percent_rank cume_dist ntile
1  5          1        1          1         0.00       0.2     1
2  1          5        5          4         1.00       1.0     3
3  3          2        2          2         0.25       0.4     1
4  2          3        3          3         0.50       0.8     2
5  2          4        3          3         0.50       0.8     2
6 NA         NA       NA         NA           NA        NA    NA

スコアの高い順にランキングしたい場合は、desc関数を使用します。

Offsets(lead , lag)

lead 、lag関数を使用すると、前後のレコードの値を取得でき、前日比、前週比等の値が簡単に出せるようになります。

lead、lagの説明

関数 説明
lead xを前方にnだけずらして、後方をdefaultで埋める
lag xを後方にnだけずらして、前方をdefaultで埋める

lead、lagの構文

R
> lead
function (x, n = 1L, default = NA, order_by = NULL, ...) 

> lag
function (x, n = 1L, default = NA, order_by = NULL, ...) 

例1:標準的な使い方

構文
# 1つづつ前後にずらす
data.frame(x = c(1, 2, 3, 4, 5)) %>%
   dplyr::mutate(lead=lead(x)) %>%
   dplyr::mutate(lag=lag(x))

  x lead lag
1 1    2  NA
2 2    3   1
3 3    4   2
4 4    5   3
5 5   NA   4

例2:nオプションを使用して、2つづつずらす

複数ずらすことも可能です。n=7にすると前週比とか簡単に出せるようになります。

オプションn
# 前後に2つづつずらす
data.frame(x = c(1, 2, 3, 4, 5)) %>%
  dplyr::mutate(lead=lead(x , n=2)) %>%
  dplyr::mutate(lag=lag(x , n=2))

  x lead lag
1 1    3  NA
2 2    4  NA
3 3    5   1
4 4   NA   2
5 5   NA   3

例3:defaultオプションを使用して、0で埋める

ずらして欠損値となる部分に埋める値を指定できます。

オプションdefault
# 値がないところを、0埋め
data_frame(x = c(1:5)) %>%
  dplyr::mutate(lead=lead(x , default=0, n=2 )) %>%
  dplyr::mutate(lag=lag(x , default=0 , n=2))

  x lead lag
1 1    3   0
2 2    4   0
3 3    5   1
4 4    0   2
5 5    0   3

例4:order_byオプションを使用して、ソート列を指定する

order_byで、ソート列を指定できます。逆順にする場合は、descを使用します。

オプションorder_by
# yの値でランキング
data_frame(x = c(1 , 2, 3, 4, 5), y=c(5,4,2,1,3)) %>%
   dplyr::mutate(lead=lead(x , default=0, order_by=y)) %>%
   dplyr::mutate(lag=lag(x , default=0 , order_by=y))

  x y lead lag
1 1 5    0   2
2 2 4    1   5
3 3 2    5   4
4 4 1    3   0
5 5 3    2   3

例5:group_by

group_byを使用すると、グループ内でのoffsetが可能です。

data_frame(x = c(1 , 2, 3, 4, 5) , y=c("aa" ,"aa" , "aa" , "bb" , "bb")) %>%
   dplyr::group_by(y) %>%
   dplyr::mutate(lead=lead(x , default=0)) %>%
   dplyr::mutate(lag=lag(x , default=0))

  x  y lead lag
1 1 aa    2   0
2 2 aa    3   1
3 3 aa    0   2
4 4 bb    5   0
5 5 bb    0   4

累積関数

各項目の累積に対して、集約関数をかけます。
言葉で書くよりも、図で書いた方が分かりやすいので、例にイメージ図を書いてみました。

主な累積関数

関数 説明
cumsum 部分和
cummin 部分min
cummax 部分max
cummean 部分平均
cumall 部分all
cumany 部分any

累積関数の構文

構文
> cumsum
function (x)

> cummin
function (x)

> cummax
function (x)

> cummean
function (x) 

> cumall
function (x) 

> cumany
function (x) 

例1:cumsum、cummin、cummax、cummean

累積に対して、集約関数をかけるイメージ
cumsum.png

data_frame(x = c(1,2,3,4,5)) %>%
   dplyr::mutate(cumsum=cumsum(x)) %>%
   dplyr::mutate(cummin=cummin(x)) %>%
   dplyr::mutate(cummax=cummax(x)) %>%
   dplyr::mutate(cummean=cummean(x))

  x cumsum cummin cummax cummean
1 1      1      1      1     1.0
2 2      3      1      2     1.5
3 3      6      1      3     2.0
4 4     10      1      4     2.5
5 5     15      1      5     3.0

例2:cumall、cumany

cumallは、累積のすべてがTRUEの場合にTRUE
cumanyは、累積のいずれか一つがTRUEの場合にTRUE
cumall.png

data_frame(x = c(TRUE , TRUE ,  FALSE ,TRUE , TRUE)) %>%
   dplyr::mutate(cumall=cumall(x)) %>%
   dplyr::mutate(cumany=cumany(x))

      x cumall cumany
1  TRUE   TRUE   TRUE
2  TRUE   TRUE   TRUE
3 FALSE  FALSE   TRUE
4  TRUE  FALSE   TRUE
5  TRUE  FALSE   TRUE

ローリング関数

RcppRollを用いることで、指定ウィンドウサイズ内で集約関数を使用することができます。
移動平均等も簡単に出せるので、便利です。
ローリング関数を使用するには、RcppRollパッケージのインストールが必要です。

パッケージインストール&読み込み
install.packages("RcppRoll")
library(RcppRoll)

ローリング関数も、文字で説明してもなかなかイメージしづらいので、イメージ図を作成しました。

rolling_function.png

主なローリング関数

関数 説明
roll_max 指定ウィンドウサイズ内の最大値を取得
roll_min 指定ウィンドウサイズ内の最小値を取得
roll_mean 指定ウィンドウサイズ内の平均値を取得
roll_median 指定ウィンドウサイズ内の中央値を取得
roll_sum 指定ウィンドウサイズ内の合計を取得
roll_prod 指定ウィンドウサイズ内の総積を取得
roll_var 指定ウィンドウサイズ内の分散を取得
roll_sd 指定ウィンドウサイズ内の標準偏差を取得

主なローリング関数の構文

構文
> roll_max
function (x, n = 1L, weights = NULL, by = 1L, fill = numeric(0), 
    partial = FALSE, align = c("center", "left", "right"), normalize = TRUE, 
    na.rm = FALSE) 

> roll_min
function (x, n = 1L, weights = NULL, by = 1L, fill = numeric(0), 
    partial = FALSE, align = c("center", "left", "right"), normalize = TRUE, 
    na.rm = FALSE) 

> roll_mean
function (x, n = 1L, weights = NULL, by = 1L, fill = numeric(0), 
    partial = FALSE, align = c("center", "left", "right"), normalize = TRUE, 
    na.rm = FALSE) 

> roll_median
function (x, n = 1L, weights = NULL, by = 1L, fill = numeric(0), 
    partial = FALSE, align = c("center", "left", "right"), normalize = TRUE, 
    na.rm = FALSE) 

> roll_sum
function (x, n = 1L, weights = NULL, by = 1L, fill = numeric(0), 
    partial = FALSE, align = c("center", "left", "right"), normalize = TRUE, 
    na.rm = FALSE) 

> roll_prod
function (x, n = 1L, weights = NULL, by = 1L, fill = numeric(0), 
    partial = FALSE, align = c("center", "left", "right"), normalize = TRUE, 
    na.rm = FALSE) 

> roll_var
function (x, n = 1L, weights = NULL, by = 1L, fill = numeric(0), 
    partial = FALSE, align = c("center", "left", "right"), normalize = TRUE, 
    na.rm = FALSE) 

> roll_sd
function (x, n = 1L, weights = NULL, by = 1L, fill = numeric(0), 
    partial = FALSE, align = c("center", "left", "right"), normalize = TRUE, 
    na.rm = FALSE) 

主なオプションの説明

オプション 説明
n ウィンドウサイズ
weights ウィンドサイズと同じ長さのベクトルで、各要素の重みを指定します。
fill 欠損値を何で埋めるかを指定します。
align ウインドウの位置を指定します。"left"、"center"、"right"が指定できます。
normalize 重みをNormalizeするかどうかを指定します。
na.rm NAを削除するかどうかを指定します。

例1:alignの違い

roll_max
# Windowサイズn=3で、最大のものを取得する例
# nが1か全体でなければ、fillの指定が必要
# align="center" : n=3の場合、該当行、およびその前後の1行を使用し、最大値を出す
# align="left"   : n=3の場合、該当行、およびその後ろ2行を使用し、最大値を出す
#    roll_maxl関数は、roll_max(x , align="left")と同様
# align="right"  : n=3の場合、該当行、およびその前2行を使用し、最大値を出す 
#    roll_maxr関数は、roll_max(x , align="right")と同様
data_frame(x = c(1:10) ) %>%
   dplyr::mutate(roll_max_center= roll_max(x , n=3 ,align="center" , fill=NA)) %>%
   dplyr::mutate(roll_max_left  = roll_max(x , n=3 ,align="left"   , fill=NA)) %>%
   dplyr::mutate(roll_max_right = roll_max(x , n=3 ,align="right"  , fill=NA))

    x roll_max_center roll_max_left roll_max_right
1   1              NA             3             NA
2   2               3             4             NA
3   3               4             5              3
4   4               5             6              4
5   5               6             7              5
6   6               7             8              6
7   7               8             9              7
8   8               9            10              8
9   9              10            NA              9
10 10              NA            NA             10

例2:重み付き移動平均

weightsオプションで重みを指定することができます。

weightsオプション
# weightsには、ウィンドウサイズnを同じ長さのベクトルを指定します。
data_frame(x = c(1:10) ) %>%
   dplyr::mutate(roll_mean=roll_mean(x , n=3 ,fill=NA)) %>%
   dplyr::mutate(roll_mean112=roll_mean(x , n=3 , weights=c(1,1,2) ,fill=NA)) %>%
   dplyr::mutate(roll_mean211=roll_mean(x , n=3 , weights=c(2,1,1) ,fill=NA)) %>%
   dplyr::mutate(roll_mean121=roll_mean(x , n=3 , weights=c(1,2,1) ,fill=NA))

    x roll_mean roll_mean112 roll_mean211 roll_mean121
1   1        NA           NA           NA           NA
2   2         2         2.25         1.75            2
3   3         3         3.25         2.75            3
4   4         4         4.25         3.75            4
5   5         5         5.25         4.75            5
6   6         6         6.25         5.75            6
7   7         7         7.25         6.75            7
8   8         8         8.25         7.75            8
9   9         9         9.25         8.75            9
10 10        NA           NA           NA           NA

例3:fillオプション指定

欠損値が出る場合、fillで指定した値が使用されます。

fill
data_frame(x = c(1:10) ) %>%
   dplyr::mutate(roll_mean1=roll_mean(x , n=3 , fill=0)) %>%
   dplyr::mutate(roll_mean2=roll_mean(x , n=3 , fill=-1)) %>%
   dplyr::mutate(roll_mean3=roll_mean(x , n=3 , fill=NA))

    x roll_mean1 roll_mean2 roll_mean3
1   1          0         -1         NA
2   2          2          2          2
3   3          3          3          3
4   4          4          4          4
5   5          5          5          5
6   6          6          6          6
7   7          7          7          7
8   8          8          8          8
9   9          9          9          9
10 10          0         -1         NA

Window関数をfilterに使用する

これまでは、主に列を追加する際にWindow関数を使用していましたが、filter関数にも使用することができます。

例1:yでグルーピングしてmin_rankが1のもののみ抽出

ranking
data_frame(x = c(1 , 2, 3, 4, 5) , y=c("aa" ,"aa" , "aa" , "bb" , "bb")) %>%
   dplyr::group_by(y) %>%
   dplyr::filter(min_rank(x) == 1)

  x  y
1 1 aa
2 4 bb

例2:累積和が5を超えるもののみ抽出

累積
data_frame(x = c(1,2,3,4,5)) %>%
   dplyr::filter(cumsum(x)>5)

  x
1 3
2 4
3 5

まとめ

Window関数編は以上になります。
分かりにくいところもあるので、今回は具体的なイメージ図を入れてみました。
使いこなせると、普段の集計には殆ど困ることは無くなると思いますので、是非、使いこなせるようになりましょう!

これだけ解説しても、まだまだ機能が残ってるdplyr、恐るべし・・・。
きっとあと1回か、2回ぐらい続きます。