ウィンドウ機能

ウィンドウ関数 は、集約関数のバリエーションの一つです。 sum() や mean() のような集約関数がn個の入力を受けて1つの値を返すのに対して、窓関数はn個の値を返します。 窓関数の出力はそのすべての入力値に依存するので、窓関数には +round() のような要素ごとに動作する関数は含まれません。 窓関数には、cumsum()cummean() のような集約関数のバリエーションや、rank() のような順位付けを行う関数、lead()lag() のようなオフセットを取る関数などがあります。

この vignette では、Lahman batting datasetの小さなサンプルを使って、賞を獲得した選手を含めてみます。

library(Lahman)

batting <- Lahman::Batting %>%
  as_tibble() %>%
  select(playerID, yearID, teamID, G, AB:H) %>%
  arrange(playerID, yearID, teamID) %>%
  semi_join(Lahman::AwardsPlayers, by = "playerID")

players <- batting %>% group_by(playerID)

ウィンドウ関数は,mutate()filter() と組み合わせて,さまざまな問題を解決します。 ここではその一部を紹介します。

# For each player, find the two years with most hits
filter(players, min_rank(desc(H)) <= 2 & H > 0)
# Within each player, rank each year by the number of games played
mutate(players, G_rank = min_rank(G))

# For each player, find every year that was better than the previous year
filter(players, G > lag(G))
# For each player, compute avg change in games played per year
mutate(players, G_change = (G - lag(G)) / (yearID - lag(yearID)))

# For each player, find all years where they played more games than they did on average
filter(players, G > mean(G))
# For each, player compute a z score based on number of games played
mutate(players, G_z = (G - mean(G)) / sd(G))

この vignette を読む前に、mutate()filter()に慣れておく必要があります。

ウィンドウ関数の種類

ウィンドウ関数には大きく分けて5つの種類があります。 2つのファミリーは集約関数とは無関係である。

他の3つのファミリーは,おなじみの集計関数のバリエーションです.

各ファミリーについては、一般的な目的と dplyr での使用方法に焦点を当てて、以下で詳しく説明します。詳細については、各関数のドキュメントを参照してください。

ランキング関数

ランキング関数は同じテーマのバリエーションで、tie の扱い方が異なります。

x <- c(1, 1, 2, 2, 2)

row_number(x)
#> [1] 1 2 3 4 5
min_rank(x)
#> [1] 1 1 3 3 3
dense_rank(x)
#> [1] 1 1 2 2 2

Rに精通している方であれば,row_number()min_rank() が基本の rank() 関数と ties.method 引数の様々な値を使って計算できることをご存知かもしれません。 これらの関数は,タイプミスを少しでも減らし,RとSQLの変換を容易にするために提供されています.

他の2つのランキング関数は0と1の間の数値を返します。 percent_rank() はランクのパーセンテージを,cume_dist() は現在の値以下の値の割合を表します。

cume_dist(x)
#> [1] 0.4 0.4 1.0 1.0 1.0
percent_rank(x)
#> [1] 0.0 0.0 0.5 0.5 0.5

これらは、各グループ内の上位10%のレコードを(例えば)選択したい場合に便利です。例えば:

filter(players, cume_dist(desc(G)) < 0.1)
#> # A tibble: 1,047 x 7
#> # Groups:   playerID [954]
#>   playerID  yearID teamID     G    AB     R     H
#>   <chr>      <int> <fct>  <int> <int> <int> <int>
#> 1 aaronha01   1963 ML1      161   631   121   201
#> 2 aaronha01   1968 ATL      160   606    84   174
#> 3 abbotji01   1991 CAL       34     0     0     0
#> 4 abernte02   1965 CHN       84    18     1     3
#> # … with 1,043 more rows

最後に ntile() はデータを n 個の均等なサイズのバケットに分割します。 これは粗いランキングで、mutate()と一緒に使うことで、データをバケットに分割してさらに要約することができます。 例えば、ntile()を使って、チーム内の選手を4つのランクのグループに分け、それぞれのグループの平均試合数を計算することができます。

by_team_player <- group_by(batting, teamID, playerID)
by_team <- summarise(by_team_player, G = sum(G))
#> `summarise()` regrouping output by 'teamID' (override with `.groups` argument)
by_team_quartile <- group_by(by_team, quartile = ntile(G, 4))
summarise(by_team_quartile, mean(G))
#> `summarise()` ungrouping output (override with `.groups` argument)
#> # A tibble: 4 x 2
#>   quartile `mean(G)`
#>      <int>     <dbl>
#> 1        1      26.7
#> 2        2      97.0
#> 3        3     270. 
#> 4        4     975.

すべてのランキング関数は、小さな入力値が小さなランクを得るように、最小から最大へとランク付けします。 最高から最低へのランク付けには desc() を使います。

lead と lag

lead()lag() は入力ベクトルのオフセットを生成し、元のベクトルに先行するか後続するかを決定する。

x <- 1:5
lead(x)
#> [1]  2  3  4  5 NA
lag(x)
#> [1] NA  1  2  3  4

以下のような用途に使用できます。

lead()lag() には,オプションの引数として order_by があります。 これがセットされていると、どの値が他の値より前に来るかを決定するために行の順序を使用する代わりに、別の変数を使用します。 これは、データをまだソートしていない場合や、ある方法でソートして別の方法で遅延させたい場合に重要です。

ここでは、必要なときに order_by を指定しなかった場合の簡単な例を紹介します。

df <- data.frame(year = 2000:2005, value = (0:5) ^ 2)
scrambled <- df[sample(nrow(df)), ]

wrong <- mutate(scrambled, prev_value = lag(value))
arrange(wrong, year)
#>   year value prev_value
#> 1 2000     0          4
#> 2 2001     1          0
#> 3 2002     4          9
#> 4 2003     9         16
#> 5 2004    16         NA
#> 6 2005    25          1

right <- mutate(scrambled, prev_value = lag(value, order_by = year))
arrange(right, year)
#>   year value prev_value
#> 1 2000     0         NA
#> 2 2001     1          0
#> 3 2002     4          1
#> 4 2003     9          4
#> 5 2004    16          9
#> 6 2005    25         16

累積集計

base Rは累積和(cumsum())、累積最小(cummin())、累積最大(cummax())を提供しています。 (また、cumprod()も提供されていますが、これはほとんど役に立ちません)。 他の一般的な累積関数としては,cumany()cumall()||&& の累積版、cummean()、累積平均があります。 これらは base R には含まれていませんが、効率的なバージョンは dplyr で提供されています。

cumany()cumall() は,ある条件が最初に(あるいは最後に)真となったときに,それまでのすべての行、あるいはそれ以降のすべての行を選択するのに便利です。 例えば,cumany()を使って、ある選手が150試合出場した年以降のすべての記録を探すことができます。

filter(players, cumany(G > 150))

lead と lag のように、蓄積される順序を制御したい場合があります。 内蔵の関数には order_by という引数がありませんので、dplyr はヘルパーを提供します: order_by()です。 order_by() には,順序付けしたい変数と,ウィンドウ関数への呼び出しを与えます.

x <- 1:10
y <- 10:1
order_by(y, cumsum(x))
#>  [1] 55 54 52 49 45 40 34 27 19 10

代わりに、よりシンプルで簡潔な with_order() を使用してください。

リサイクルアグリゲート

Rのベクトルリサイクルは、要約よりも高い値や低い値を簡単に選択することができます。 これをリサイクル集約と呼んでいるのは、集約の値が元のベクトルと同じ長さにリサイクルされるからです。 リサイクル集計は、平均値よりも大きい、または中央値よりも小さいすべてのレコードを見つけたい場合に便利です。

filter(players, G > mean(G))
filter(players, G < median(G))

ほとんどのSQLデータベースでは,median()quantile() に相当するものはありませんが,フィルタリングを行う場合には ntile() で同じ効果を得ることができます。 例えば,x > median(x)ntile(x, 2) == 2 と同等であり、x > quantile(x, 75)ntile(x, 100) > 75ntile(x, 4) > 3 と同等です。

filter(players, ntile(G, 2) == 2)

このアイデアを使って、あるフィールドの値が最も高い(x == max(x))、 または最も低い(x == min(x))レコードを選択することもできますが、ランキング関数の方が同値をよりコントロールでき、 任意の数のレコードを選択することができます。

リサイクル集計は,mutate() と組み合わせて使うこともできます。 例えば、バッティングのデータを使って、選手がリーグに入ってからプレーした年数である「career year」を計算することができます。

mutate(players, career_year = yearID - min(yearID) + 1)
#> # A tibble: 19,978 x 8
#> # Groups:   playerID [1,359]
#>   playerID  yearID teamID     G    AB     R     H career_year
#>   <chr>      <int> <fct>  <int> <int> <int> <int>       <dbl>
#> 1 aaronha01   1954 ML1      122   468    58   131           1
#> 2 aaronha01   1955 ML1      153   602   105   189           2
#> 3 aaronha01   1956 ML1      153   609   106   200           3
#> 4 aaronha01   1957 ML1      151   615   118   198           4
#> # … with 19,974 more rows

また、入門編の例のように、z-scoreを計算することもできます。

mutate(players, G_z = (G - mean(G)) / sd(G))
#> # A tibble: 19,978 x 8
#> # Groups:   playerID [1,359]
#>   playerID  yearID teamID     G    AB     R     H    G_z
#>   <chr>      <int> <fct>  <int> <int> <int> <int>  <dbl>
#> 1 aaronha01   1954 ML1      122   468    58   131 -1.16 
#> 2 aaronha01   1955 ML1      153   602   105   189  0.519
#> 3 aaronha01   1956 ML1      153   609   106   200  0.519
#> 4 aaronha01   1957 ML1      151   615   118   198  0.411
#> # … with 19,974 more rows