14  データハンドリング [要約]

前章ではデータの一部(subset)を抽出する方法について説明しましたが、本章はデータを拡張する、あるいは全く別のデータが得られるような処理について解説します。後者は主に元のデータを要約し(記述統計量)、その結果を出力する方法で、前者はデータ内の変数に基づき、指定された計算を行った結果を新しい列として追加する方法です。今回も前章と同じデータを使用します。

pacman::p_load(tidyverse)
# データのパスは適宜修正すること
# 文字化けが生じる場合、以下のコードに書き換える。
# df <- read_csv("Data/Ramen.csv", locale = locale(encoding = "utf8"))
df <- read_csv("Data/Ramen.csv")

データの詳細については第13.3章を参照してください。

14.1 記述統計量の計算

14.1.1 summarise()による記述統計量の計算

ある変数の平均値や標準偏差、最小値、最大値などの記述統計量(要約統計量)を計算することも可能です。これはsummarize()またはsummarise()関数を使いますが、この関数は後で紹介するgroup_by()関数と組み合わせることで力を発揮します。ここではグルーピングを考えずに、全データの記述統計量を計算する方法を紹介します。

summarise()関数の使い方は以下の通りです。

# summarise()関数の使い方
データフレーム名 |>
  summarise(新しい変数名 = 関数名(計算の対象となる変数名))

もし、Score変数の平均値を計算し、その結果をMeanという列にしたい場合は以下のようなコードになります。

df |>
  summarise(Mean = mean(Score))
# A tibble: 1 × 1
   Mean
  <dbl>
1    NA

ただし、mean()関数は欠損値が含まれるベクトルの場合、NAを返します。この場合方法は2つ考えられます。

  1. filter()関数を使ってScoreが欠損しているケースを予め除去する。
  2. na.rm引数を指定し、欠損値を除去した平均値を求める。

ここでは2番目の方法を使います。

df |>
  summarise(Mean = mean(Score, na.rm = TRUE))
# A tibble: 1 × 1
   Mean
  <dbl>
1  3.66

dfScore変数の平均値はNAであることが分かります。また、summarise()関数は複数の記述統計量を同時に計算することも可能です。以下はScore変数の平均値、中央値、標準偏差、最小値、最大値、第一四分位点、第三四分位点を計算し、Score.Descという名のデータフレームに格納するコードです。

Score.Desc <- df |>
  summarize(Mean   =     mean(Score,       na.rm = TRUE),  # 平均値
            Median =   median(Score,       na.rm = TRUE),  # 中央値
            SD     =       sd(Score,       na.rm = TRUE),  # 標準偏差
            Min    =      min(Score,       na.rm = TRUE),  # 最小値
            Max    =      max(Score,       na.rm = TRUE),  # 最大値
            Q1     = quantile(Score, 0.25, na.rm = TRUE),  # 第一四分位点
            Q3     = quantile(Score, 0.75, na.rm = TRUE))  # 第三四分位点
Score.Desc
# A tibble: 1 × 7
   Mean Median    SD   Min   Max    Q1    Q3
  <dbl>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1  3.66   3.58 0.719     1     5     3     4

むろん、複数の変数に対して記述統計量を計算することも可能です。たとえば、平均予算 (Budget)、口コミ数 (ScoreN)、口コミ評価 (Score)の平均値を求めるとしたら、

df |>
  summarize(Budget_Mean = mean(Budget, na.rm = TRUE), # 平均予算の平均値
            SocreN_Mean = mean(ScoreN, na.rm = TRUE), # 口コミ数の平均値
            Score_Mean  = mean(Score,  na.rm = TRUE)) # 評価の平均値
# A tibble: 1 × 3
  Budget_Mean SocreN_Mean Score_Mean
        <dbl>       <dbl>      <dbl>
1       1232.       0.537       3.66

のように書きます。実はsummarise()はこれくらいで十分便利です。ただし、以上の操作はもっと簡単なコードに置換できます。ただし、ラムダ関数など、やや高度な内容になるため、以下の内容は飛ばして、次の節 (グルーピング)を読んでいただいても構いません。

まずは、複数の変数に対して同じ記述統計量を求める例を考えてみましょう。たとえば、BudgetScoreNScoreに対して平均値を求める例です。これはacross()関数を使うとよりコードが短くなります。まずはacross()関数の書き方から見ましょう。

# across()の使い方
データフレーム名 |>
  summarise(across(変数名のベクトル, ラムダ関数))

変数名のベクトルは長さ1以上のベクトルです。たとえば、BudgetScoreNScoreの場合c(Budget, ScoreN, Score)になります。これはdf内で隣接する変数ですからBudget:Scoreの書き方も使えます。また、where()any_of()starts_with()のような関数を使って変数を指定することも可能です。ラムダ関数(lambda function; ラムダ式)は~で始まる関数であり、無名関数とも呼ばれます。たとえば、平均値を計算するラムダ関数は~mean()であり、第一引数は.xとなります。この.xacross()の第一引数(変数たち)が入ります。また、()内には更に引数を指定することもでき、たとえば欠損値を除外して計算するna.rm = TRUEなどが追加できます。ラムダ関数の詳細は後でもう一度解説するとし、とりあえずBudgetScoreNScoreの平均値を計算してみましょう。

df |>
  summarize(across(Budget:Score, ~mean(.x, na.rm = TRUE)))
# A tibble: 1 × 3
  Budget ScoreN Score
   <dbl>  <dbl> <dbl>
1  1232.  0.537  3.66

across()使わない場合、4行必要だったコードが2行になりました。変数が少ない場合はacross()を使わない方が、可読性が高くなる場合もあります。しかし、変数が多くなる場合、可読性がやや落ちてもacross()を使った方が効率的でしょう。

次は、ある変数に対して複数の記述統計量を計算したい場合について考えます。BudgetScoreNScore変数の第一四分位点と第三四分位点をacross()を使わずに計算すると家のような7行のコードになります。

df |>
  summarize(Budget_Q1 = quantile(Budget, 0.25, na.rm = TRUE),
            Budget_Q3 = quantile(Budget, 0.75, na.rm = TRUE),
            ScoreN_Q1 = quantile(ScoreN, 0.25, na.rm = TRUE),
            ScoreN_Q3 = quantile(ScoreN, 0.75, na.rm = TRUE),
            Score_Q1  = quantile(Score,  0.25, na.rm = TRUE),
            Score_Q3  = quantile(Score,  0.75, na.rm = TRUE))
# A tibble: 1 × 6
  Budget_Q1 Budget_Q3 ScoreN_Q1 ScoreN_Q3 Score_Q1 Score_Q3
      <dbl>     <dbl>     <dbl>     <dbl>    <dbl>    <dbl>
1       800      1000         0         0        3        4

この作業もacross()を使ってより短縮することができます。ここではラムダ関数の知識が必要になります。ラムダ関数とは関数名を持たない無名関数 (anonymous functions)を意味しますが、詳細は割愛します。興味のある読者はWikipediaなどを参照してください。簡単にいうとその場で即席に関数を作成し、計算が終わったら破棄する関数です。ラムダ関数の書き方にはバリエーションがありますが、ここでは{purrr}パッケージのラムダ関数スタイルを使用します1。まずは、書き方から確認します。

# ラムダ関数を用いたacross()の使い方
データフレーム名 |>
  summarise(across(変数名のベクトル, .fns = list(結果の変数名 = ラムダ関数)))
ラムダ関数の3つの書き方

 ここでは~mean(.x, na.rm = TRUE)と書いたが、他の書き方もある。昔からのやり方ではfunction(x) mean(x, na.rm = TRUE)のような書き方がある。関数内の内容が複数行に渡る場合はfunction(x) { mean(x, na.rm = TRUE) }と表記する。そしてR 4.1から追加された\(x) mean(x, na.rm = TRUE)のような書き方もある。以下のコードはすべて同じ結果を返す。Rネイティブの書き方の場合、引数は.xでなく、xであることに注意しよう(\(y)ならyが引数となる)。

# purrrスタイル
df |>
  summarize(across(Budget:Score, ~mean(.x, na.rm = TRUE)))

# R nativeスタイル
df |>
  summarize(across(Budget:Score, function(x) mean(x, na.rm = TRUE)))

# R nativeスタイル
df |>
  summarize(across(Budget:Score, function(x) { mean(x, na.rm = TRUE) }))

# R nativeスタイル(R 4.1以降)
# function を \ に省略したもの
df |>
  summarize(across(Budget:Score, \(x) mean(x, na.rm = TRUE)))

先ほどの書き方と似ていますが、関数を複数書く必要があるため、今回は関数名をlist型にまとめ、.fns引数に指定します。そして、結果の変数名は結果として出力されるデータフレームの列名を指定する引数です。たとえば、Meanにすると結果は元の変数名1_Mean元の変数名2_Mean…のように出力されます。そして、ラムダ関数が実際の関数が入る箇所です。とりあえず今回はコードを走らせ、結果から確認してみましょう。

df |>
  summarize(across(Budget:Score, 
                   .fns = list(Q1 = ~quantile(.x, 0.25, na.rm = TRUE),
                               Q3 = ~quantile(.x, 0.75, na.rm = TRUE))))
# A tibble: 1 × 6
  Budget_Q1 Budget_Q3 ScoreN_Q1 ScoreN_Q3 Score_Q1 Score_Q3
      <dbl>     <dbl>     <dbl>     <dbl>    <dbl>    <dbl>
1       800      1000         0         0        3        4

結果の列名がBudget_Q1Budget_Q3ScoreN_Q1…のようになり、それぞれの変数の第一四分位点と第三四分位点が出力されます。問題はラムダ関数の方ですが、普通の関数に非常に近いことが分かります。across()内のラムダ関数は~関数名(.x, その他の引数)のような書き方になります。関数名の前に~が付いていることに注意してください。分位数を求める関数はquantile()であり、quantile(ベクトル, 分位数)であり、必要に応じてna.rmを付けます。この分位数が0.25なら第一四分位点、0.5なら第二四分位点 (=中央値)、0.75なら第三四分位点になります。それではラムダ関数~quantile(.x, 0.25, na.rm = TRUE)はどういう意味でしょうか。これは.xの箇所にBudgetScoreNScoreが入ることを意味します。.xという書き方は決まりです。.yとか.Song-san-Daisukiなどはダメです。そして、0.25を付けることによって第一四分位点を出力するように指定します。また、BudgetScoreNScoreに欠損値がある場合、無視するようにna.rm = TRUEを付けます。

ラムダ関数を第11章で解説した自作関数で表現すると、以下のようになります。

# 以下の3つは同じ機能をする関数である

# ラムダ関数
~quantile(.x, 0.25, na.rm = TRUE)

# 一般的な関数の書き方1
名無し関数 <- function(x) {
  quantile(x, 0.25, na.rm = TRUE)
}

# 一般的な関数の書き方2
名無し関数 <- function(x) quantile(x, 0.25, na.rm = TRUE)

この3つは全て同じですが、ラムダ関数は関数名を持たず、その場で使い捨てる関数です。むろん、ラムダ関数を使わずに事前に第一四分位点と第三四分位点を求める関数を予め作成し、ラムダ関数の代わりに使うことも可能です。まずは第一四分位点と第三四分位点を求める自作関数FuncQ1FuncQ2を作成します。

# ラムダ関数を使わない場合は事前に関数を定義しておく必要がある
FuncQ1 <- function(x) {
  quantile(x, 0.25, na.rm = TRUE)
}
FuncQ3 <- function(x) {
  quantile(x, 0.75, na.rm = TRUE)
}

後は先ほどのほぼ同じ書き方ですが、今回はラムダ関数を使わないため関数名に~を付けず、関数名のみで十分です。()も不要です。

# やっておくと、summarise()文は簡潔になる
df |>
  summarize(across(Budget:Score, list(Q1 = FuncQ1, Q3 = FuncQ3)))
# A tibble: 1 × 6
  Budget_Q1 Budget_Q3 ScoreN_Q1 ScoreN_Q3 Score_Q1 Score_Q3
      <dbl>     <dbl>     <dbl>     <dbl>    <dbl>    <dbl>
1       800      1000         0         0        3        4

事前に関数を用意するのが面倒ですが、across()の中身はかなりスッキリしますね。もし、このような作業を何回も行うなら、ラムダ関数を使わず、自作関数を用いることも可能です。ただし、自作関数であっても引数が2つ以上必要な場合はラムダ関数を使います。

14.1.2 summarise()に使える便利な関数

以下の内容は後で説明するgroup_by()関数を使っているため、まだgroup_by()に馴染みのない読者はまずはここを読み飛ばし、グルーピングの節にお進みください。

IQR(): 四分位範囲を求める

四分位範囲は第三四分位点から第一四分位点を引いた値であり、Rの内蔵関数であるIQR()を使えば便利です。この関数はmeansd()関数と同じ使い方となります。

df |>
  filter(!is.na(Walk)) |> # 予め欠損したケースを除くと、後でna.rm = TRUEが不要
  group_by(Pref) |>
  summarise(Mean    = mean(Walk),
            SD      = sd(Walk),
            IQR     = IQR(Walk),
            N       = n(),
            .groups = "drop") |>
  arrange(Mean)
# A tibble: 9 × 5
  Pref      Mean    SD   IQR     N
  <chr>    <dbl> <dbl> <dbl> <int>
1 東京都    4.29  4.49     4   919
2 大阪府    5.92  6.08     6   932
3 神奈川県  8.21  7.91    10   878
4 京都府    8.38  6.95     9   339
5 兵庫県    8.52  7.27    10   484
6 奈良県   10.6   6.59    10   123
7 千葉県   10.6   8.21    12   776
8 埼玉県   11.6   8.99    14   817
9 和歌山県 12.8   6.83     9   107

first()last()nth(): n番目の要素を求める

稀なケースかも知れませんが、データ内、またはグループ内のn番目の行を抽出する時があります。たとえば、市区町村の情報が格納されているデータセットで、人口が大きい順でデータがソートされているとします。各都道府県ごとに最も人口が大きい市区町村のデータ、あるいは最も少ない市区町村のデータが必要な際、first()last()関数が有効です。

それでは各都道府県ごとに「最も駅から遠いラーメン屋」の店舗名と最寄りの駅からの徒歩距離を出力したいとします。まずは、徒歩距離のデータが欠損しているケースを除去し、データを徒歩距離順でソートします。これはfilter()arrange()関数を使えば簡単です。続いて、group_by()を使って都府県単位でデータをグループ化します。最後にsummarise()関数内にlast()関数を使います。データは駅から近い順に鳴っているため、各都府県内の最後の行は駅から最も遠い店舗になるからです。

df |>
  filter(!is.na(Walk)) |>
  arrange(Walk) |>
  group_by(Pref) |>
  summarise(Farthest  = last(Name),
            Distance  = last(Walk))
# A tibble: 9 × 3
  Pref     Farthest                           Distance
  <chr>    <chr>                                 <dbl>
1 京都府   熱烈らぁめん                             30
2 兵庫県   濃厚醤油 中華そば いせや 玉津店          43
3 千葉県   札幌ラーメン どさん子 佐原51号店         59
4 和歌山県 中華そば まる乃                          30
5 埼玉県   札幌ラーメン どさん子 小鹿野店          116
6 大阪府   河童ラーメン本舗 岸和田店                38
7 奈良県   博多長浜らーめん 夢街道 四条大路店       29
8 東京都   てんがら 青梅新町店                      30
9 神奈川県 札幌ラーメン どさん子 中津店             73

このlast()first()に変えると、最寄りの駅から最も近い店舗情報が表示されます。また、「n番目の情報」が必要な際はnth()関数を使います。nth(Name, 2)に変えることで2番目の店舗名が抽出できます。

n_distinct(): ユニーク値の個数を求める

n_distinct()は何種類の要素が含まれているかを計算する関数であり、length(unique())関数と同じ機能をします。たとえば、以下のmyVec1に対して何種類の要素があるかを確認してみましょう。

myVec1 <- c("A", "B", "B", "D", "A", "B", "D", "C", "A")

unique(myVec1)
[1] "A" "B" "D" "C"

myVec1"A""B""D""C"の要素で構成されていることが分かります。これがmyVec1ユニーク値 (unique values)です。そして、このユニーク値の個数を調べるためにlength()を使います。

length(unique(myVec1))
[1] 4

これでmyVec1は4種類の値が存在することが分かります。これと全く同じ機能をする関数がn_distinct()です。

n_distinct(myVec1)
[1] 4

この関数をsummarise()に使うことで、都府県ごとに駅の個数が分かります。あるいは「東京都内の選挙区に、これまでの衆院選において何人の候補者が存在したか」も分かります。ここではdf内の都府県ごとに駅の個数を計算してみましょう。最後の駅数が多い順でソートします。

df |>
  filter(!is.na(Station)) |> # 最寄りの駅が欠損しているケースを除去
  group_by(Pref) |>
  summarise(N_Station = n_distinct(Station),
            .groups   = "drop") |>
  arrange(desc(N_Station))
# A tibble: 9 × 2
  Pref     N_Station
  <chr>        <int>
1 東京都         368
2 大阪府         341
3 千葉県         241
4 神奈川県       240
5 兵庫県         199
6 埼玉県         185
7 京都府         123
8 奈良県          52
9 和歌山県        46

当たり前かも知れませんが、駅数が最も多いのは東京都で次が大阪府であることが分かります。

any()all(): 条件に合致するか否かを求める

any()all()はベクトル内の全要素に対して条件に合致するか否かを判定する関数です。ただし、any()は一つの要素でも条件に合致すればTRUEを、全要素が合致しない場合FALSEを返します。一方、all()は全要素に対して条件を満たせばTRUE、一つでも満たさない要素があればFALSEを返します。以下はany()all()の例です。

myVec1 <- c(1, 2, 3, 4, 5)
myVec2 <- c(1, 3, 5, 7, 11)

any(myVec1 %% 2 == 0) # myVec1を2で割った場合、一つでも余りが0か
[1] TRUE
all(myVec1 %% 2 == 0) # myVec1を2で割った場合、全ての余りが0か
[1] FALSE
all(myVec2 %% 2 != 0) # myVec2を2で割った場合、全ての余りが0ではないか
[1] TRUE

それでは実際にdfに対してany()all()関数を使ってみましょう。一つ目は「ある都府県に最寄りの駅から徒歩60分以上の店舗が一つでもあるか」であり、二つ目は「ある都府県の店舗は全て最寄りの駅から徒歩30分以下か」です。それぞれの結果をOver60Within30という列で出力してみましょう。

df |>
  group_by(Pref) |>
  summarise(Over60   = any(Walk >= 60, na.rm = TRUE),
            Within30 = all(Walk <= 30, na.rm = TRUE),
            .groups  = "drop")
# A tibble: 9 × 3
  Pref     Over60 Within30
  <chr>    <lgl>  <lgl>   
1 京都府   FALSE  TRUE    
2 兵庫県   FALSE  FALSE   
3 千葉県   FALSE  FALSE   
4 和歌山県 FALSE  TRUE    
5 埼玉県   TRUE   FALSE   
6 大阪府   FALSE  FALSE   
7 奈良県   FALSE  TRUE    
8 東京都   FALSE  TRUE    
9 神奈川県 TRUE   FALSE   

埼玉県と神奈川県において、最寄りの駅から徒歩60以上の店がありました。また、京都府、東京都、奈良県、和歌山県の場合、全店舗が最寄りの駅から徒歩30分以下ということが分かります。当たり前ですがOver60TRUEならWithin30は必ずFALSEになりますね。

14.2 グルーピング

14.2.1 group_by()によるグループ化

先ほどのsummarise()関数は確かに便利ですが、特段に便利とも言いにくいです。dfScoreの平均値を計算するだけなら、summarise()関数を使わない方が楽です。

# これまでのやり方
df |>
  summarise(Mean = mean(Score, na.rm = TRUE))
# A tibble: 1 × 1
   Mean
  <dbl>
1  3.66
# 普通にこれでええんちゃう?
mean(df$Score, na.rm = TRUE)
[1] 3.663457

しかし、これをグループごとに計算するならどうでしょう。たとえば、Scoreの平均値を都府県ごとに計算するとします。この場合、以下のようなコードになります。

mean(df$Score[df$Pref == "東京都"],   na.rm = TRUE)
[1] 3.674256
mean(df$Score[df$Pref == "神奈川県"], na.rm = TRUE)
[1] 3.533931
mean(df$Score[df$Pref == "千葉県"],   na.rm = TRUE)
[1] 3.715983
mean(df$Score[df$Pref == "埼玉県"],   na.rm = TRUE)
[1] 3.641573
mean(df$Score[df$Pref == "大阪府"],   na.rm = TRUE)
[1] 3.765194
mean(df$Score[df$Pref == "京都府"],   na.rm = TRUE)
[1] 3.684976
mean(df$Score[df$Pref == "兵庫県"],   na.rm = TRUE)
[1] 3.543936
mean(df$Score[df$Pref == "奈良県"],   na.rm = TRUE)
[1] 3.854762
mean(df$Score[df$Pref == "和歌山県"], na.rm = TRUE)
[1] 3.96999

変わったのはdf$Scoredf$Score[df$Pref == "東京都"]に変わっただけです。df$Pref"東京都"であるか否かをTRUEFALSEで判定し、これを基準にdf$Scoreを抽出する仕組みです。df$Scoredf$Prefは同じデータフレームですから、このような書き方で問題ありません。

これだけでもかなり書くのが面倒ですが、これが47都道府県なら、あるいは200ヶ国ならかなり骨の折れる作業でしょう。ここで大活躍するのが{dplyr}パッケージのgroup_by()関数です。引数はグループ化する変数名だけです。先ほどの作業を{dplyr}を使うならPref変数でグループ化し、summarise()関数で平均値を求めるだけです。今回はScoreだけでなく、ScoreNの平均値も求めてみましょう。そして、評価が高い順にソートもしてみます。

# ScoreNとScoreの平均値をPrefごとに求める
df |>
  group_by(Pref) |>
  summarise(ScoreN_Mean = mean(ScoreN, na.rm = TRUE),
            Score_Mean  = mean(Score,  na.rm = TRUE)) |>
  arrange(desc(Score_Mean))
# A tibble: 9 × 3
  Pref     ScoreN_Mean Score_Mean
  <chr>          <dbl>      <dbl>
1 和歌山県       0.593       3.97
2 奈良県         0.306       3.85
3 大阪府         0.516       3.77
4 千葉県         0.259       3.72
5 京都府         0.522       3.68
6 東京都         1.17        3.67
7 埼玉県         0.278       3.64
8 兵庫県         0.389       3.54
9 神奈川県       0.587       3.53

評判が最も高い都府県は和歌山県、最も低いのは神奈川県ですね。Songも和歌山ラーメンは井出系も車庫前系も好きです。しかし、大事なのは「井出系」と「車庫前系」といった分類が正しいかどうかではありません。コードが非常に簡潔となり、ソートなども自由自在であることです。都府県ごとにScoreNScoreの平均値を求める場合、{dplyr}を使わなかったら18行のコードとなり、ソートも自分でやる必要があります。一方、group_by()関数を使うことによってコードが5行になりました。

group_by()と同じ機能を持つ.by引数

最新の{dplyr}を使う場合(2024年10月21日現在、{dplyr}1.1.4)、group_by()関数を使わず、summarise()関数内に.by引数を指定しても同じ動きをします。具体的には.by = グルーピングする変数名を追加するだけです。したがって、以下は上記のコードと同じ内容です。

df |>
  summarise(ScoreN_Mean = mean(ScoreN, na.rm = TRUE),
            Score_Mean  = mean(Score,  na.rm = TRUE),
            .by         = Pref) |>
  arrange(desc(Score_Mean))
# A tibble: 9 × 3
  Pref     ScoreN_Mean Score_Mean
  <chr>          <dbl>      <dbl>
1 和歌山県       0.593       3.97
2 奈良県         0.306       3.85
3 大阪府         0.516       3.77
4 千葉県         0.259       3.72
5 京都府         0.522       3.68
6 東京都         1.17        3.67
7 埼玉県         0.278       3.64
8 兵庫県         0.389       3.54
9 神奈川県       0.587       3.53

続いて、一つ便利な関数を紹介します。それはグループのサイズを計算する関数、n()です。この関数をsummarise()内に使うと、各グループに属するケース数を出力します2。先ほどのコードを修正し、各グループのサイズをNという名の列として追加してみましょう。そしてソートの順番はNを最優先とし、同じ場合はScore_Meanが高い方を上に出力させます。また、ScoreN_Meanの前に、口コミ数の合計も出してみましょう。

# Prefごとに口コミ数の合計、口コミ数の平均値、評価の平均値、店舗数を求める
# 店舗数-評価の平均値順でソートする
df |>
  group_by(Pref) |>
  summarise(ScoreN_Sum  = sum(ScoreN,  na.rm = TRUE),
            ScoreN_Mean = mean(ScoreN, na.rm = TRUE),
            Score_Mean  = mean(Score,  na.rm = TRUE),
            N           = n()) |>
  arrange(desc(N), desc(Score_Mean))
# A tibble: 9 × 5
  Pref     ScoreN_Sum ScoreN_Mean Score_Mean     N
  <chr>         <dbl>       <dbl>      <dbl> <int>
1 大阪府          516       0.516       3.77  1000
2 千葉県          259       0.259       3.72  1000
3 東京都         1165       1.17        3.67  1000
4 埼玉県          278       0.278       3.64  1000
5 神奈川県        587       0.587       3.53  1000
6 兵庫県          230       0.389       3.54   591
7 京都府          216       0.522       3.68   414
8 奈良県           45       0.306       3.85   147
9 和歌山県         83       0.593       3.97   140

記述統計をグループごとに求めるのは普通にあり得るケースですし、実験データの場合はほぼ必須の作業でう。統制群と処置群間においてグループサイズが均一か、共変量のバラツキが十分に小さいかなどを判断する際にgroup_by()summarise()関数の組み合わせは非常に便利です。

14.2.2 複数の変数を用いたグループ化

グループ化変数は2つ以上指定することも可能です。たとえば、都府県 (Pref)と最寄りの駅の路線 (Line)でグループ化することも可能です。それではPrefLineでグループ化し、店舗数と口コミ数、評価の平均値を計算し、ソートの順番は店舗数、店舗数が同じなら評価の平均値が高い順にしましょう。今回はTop 20まで出してみます。

# ScoreNとScoreの平均値をPrefごとに求める
df |>
  filter(!is.na(Line)) |> # Lineが欠損していないケースのみ残す
  group_by(Pref, Line) |> # PrefとLineでグループ化
  summarise(N           = n(),
            ScoreN_Sum  = sum(ScoreN,  na.rm = TRUE),
            Score_Mean  = mean(Score,  na.rm = TRUE)) |>
  arrange(desc(N), desc(Score_Mean)) |>
  print(n = 20)
`summarise()` has grouped output by 'Pref'. You can override using the
`.groups` argument.
# A tibble: 523 × 5
# Groups:   Pref [9]
   Pref     Line                        N ScoreN_Sum Score_Mean
   <chr>    <chr>                   <int>      <dbl>      <dbl>
 1 埼玉県   東武東上線                122         27       3.68
 2 東京都   JR                      104        231       3.56
 3 神奈川県 小田急小田原線             96         31       3.59
 4 埼玉県   東武伊勢崎線               96         18       3.51
 5 神奈川県 横浜市営ブルーライン       82         77       3.66
 6 千葉県   京成本線                   82         29       3.34
 7 神奈川県 京急本線                   68         40       3.33
 8 千葉県   東武野田線                 63          2       4.75
 9 神奈川県 小田急江ノ島線             62          8       3.79
10 大阪府   阪急京都本線               53         32       3.67
11 大阪府   南海本線                   52         11       4.22
12 兵庫県   阪神本線                   52         23       3.80
13 埼玉県   JR高崎線                   51          5       4   
14 兵庫県   山陽電鉄本線               51         15       2.98
15 千葉県   JR総武本線(東京-銚子)    47          8       4   
16 埼玉県   西武新宿線                 45          8       4.17
17 埼玉県   秩父鉄道線                 43         10       3.82
18 大阪府   京阪本線                   43         10       3.69
19 千葉県   新京成電鉄                 43          6       3.6 
20 京都府   阪急京都本線               43         27       3.5 
# ℹ 503 more rows

結果としては問題ないように見えますが、気になるメッセージが出力されます。

`summarise()` has grouped output by 'Pref'. You can override using the `.groups` argument.

これはLineはグルーピング変数として機能しなくなり、Pref変数のみグループ変数になったという意味です。なぜLineが解除され、Prefが残るかというと、それはgroup_by()関数内でPrefを先に指定したからです。group_by(Line, Pref)でグルーピングすると、Prefが解除され、Lineのみがグルーピング変数となります。summarise()後のグルーピング変数の扱いはsummarise()関数の.groups引数で指定できます。既定値は.groups = "drop_last"で「最後のグルーピング変数を解除する」という意味です(場合によっては既定値が"keep"になりますが、後述します)。多くの場合、summarise()後は全グルーピングを解除するのが一般的なので、この場合は.groups = "drop"と指定します。もし、グルーピングを維持したい場合は.groups = "keep"と指定します。今回の例はsummarise()内に.group = "drop"を指定し、グループ化を解除します。

df |>
  filter(!is.na(Line)) |>
  group_by(Pref, Line) |>
  summarise(N           = n(),
            ScoreN_Sum  = sum(ScoreN,  na.rm = TRUE),
            Score_Mean  = mean(Score,  na.rm = TRUE),
            .groups     = "drop") |> # グルーピングをすべて解除する
  arrange(desc(N), desc(Score_Mean)) |>
  print(n = 20)
# A tibble: 523 × 5
   Pref     Line                        N ScoreN_Sum Score_Mean
   <chr>    <chr>                   <int>      <dbl>      <dbl>
 1 埼玉県   東武東上線                122         27       3.68
 2 東京都   JR                      104        231       3.56
 3 神奈川県 小田急小田原線             96         31       3.59
 4 埼玉県   東武伊勢崎線               96         18       3.51
 5 神奈川県 横浜市営ブルーライン       82         77       3.66
 6 千葉県   京成本線                   82         29       3.34
 7 神奈川県 京急本線                   68         40       3.33
 8 千葉県   東武野田線                 63          2       4.75
 9 神奈川県 小田急江ノ島線             62          8       3.79
10 大阪府   阪急京都本線               53         32       3.67
11 大阪府   南海本線                   52         11       4.22
12 兵庫県   阪神本線                   52         23       3.80
13 埼玉県   JR高崎線                   51          5       4   
14 兵庫県   山陽電鉄本線               51         15       2.98
15 千葉県   JR総武本線(東京-銚子)    47          8       4   
16 埼玉県   西武新宿線                 45          8       4.17
17 埼玉県   秩父鉄道線                 43         10       3.82
18 大阪府   京阪本線                   43         10       3.69
19 千葉県   新京成電鉄                 43          6       3.6 
20 京都府   阪急京都本線               43         27       3.5 
# ℹ 503 more rows
group_by()と同じ機能を持つ.by引数

ちなみに.by引数でグルーピングを行う場合は自動的にグルーピングが解除されるので、.groups = "drop"は不要です。したがって、以下は上記のコードと同じ内容となります。

df |>
  filter(!is.na(Line)) |> 
  summarise(N           = n(),
            ScoreN_Sum  = sum(ScoreN,  na.rm = TRUE),
            Score_Mean  = mean(Score,  na.rm = TRUE),
            .by         = c(Pref, Line)) |>
  arrange(desc(N), desc(Score_Mean)) |>
  print(n = 20)
# A tibble: 523 × 5
   Pref     Line                        N ScoreN_Sum Score_Mean
   <chr>    <chr>                   <int>      <dbl>      <dbl>
 1 埼玉県   東武東上線                122         27       3.68
 2 東京都   JR                      104        231       3.56
 3 神奈川県 小田急小田原線             96         31       3.59
 4 埼玉県   東武伊勢崎線               96         18       3.51
 5 神奈川県 横浜市営ブルーライン       82         77       3.66
 6 千葉県   京成本線                   82         29       3.34
 7 神奈川県 京急本線                   68         40       3.33
 8 千葉県   東武野田線                 63          2       4.75
 9 神奈川県 小田急江ノ島線             62          8       3.79
10 大阪府   阪急京都本線               53         32       3.67
11 大阪府   南海本線                   52         11       4.22
12 兵庫県   阪神本線                   52         23       3.80
13 埼玉県   JR高崎線                   51          5       4   
14 兵庫県   山陽電鉄本線               51         15       2.98
15 千葉県   JR総武本線(東京-銚子)    47          8       4   
16 埼玉県   西武新宿線                 45          8       4.17
17 埼玉県   秩父鉄道線                 43         10       3.82
18 大阪府   京阪本線                   43         10       3.69
19 千葉県   新京成電鉄                 43          6       3.6 
20 京都府   阪急京都本線               43         27       3.5 
# ℹ 503 more rows

ぐるなびに登録されているラーメン屋が最も多い路線は埼玉県内の東武東上線で122店舗があります。東武東上線は東京都と埼玉県をまたがる路線ですので、東武東上線だけならもっと多いかも知れませんね。

ここで.groups引数についてもう少し考えてみたいと思います。多くの場合、.groups引数の既定値は"drop_last"ですが、場合によっては"keep"になるケースもあります。それは記述統計量の結果が長さ2以上のベクトルである場合です。平均値を求めるmean()、標準偏差を求めるsd()などは、結果として長さ1のベクトルを返します。しかし、長さ2以上ののベクトルを返す関数もあります。たとえば、分位数を求めるquantile()関数があります。quantile(ベクトル名, 0.25)の場合、第一四分位点のみ返すため、結果は長さ1のベクトルです。しかし、quantile(ベクトル名, c(0.25, 0.5, 0.75))のように第一四分位点から第三四分位点を同時に計算し、長さ3のベクトルが返されるケースもありますし、第二引数を省略すると、最小値・第一四分位点・第二四分位点・第三四分位点・最大値、つまり、長さ5のベクトルが返される場合があります。

# 第一四分位点のみを求める (長さ1のベクトル)
quantile(df$Walk, 0.25, na.rm = TRUE)
25% 
  2 
# 引数を省略する (長さ5のベクトル)
quantile(df$Walk, na.rm = TRUE)
  0%  25%  50%  75% 100% 
   1    2    5   12  116 

.groupsのデフォルト値が"keep"になるのは、このように長さ2以上のベクトルが返されるケースです。たとえば、都府県と最寄りの駅の路線でグループ化し、店舗までの徒歩距離の平均値を求めるとします。デフォルト値の変化を見るために、ここではあえて.groups引数を省略しました。

df |>
  filter(!is.na(Walk)) |>
  group_by(Pref, Line) |>
  summarise(Mean = mean(Walk))
`summarise()` has grouped output by 'Pref'. You can override using the
`.groups` argument.
# A tibble: 509 × 3
# Groups:   Pref [9]
   Pref   Line                       Mean
   <chr>  <chr>                     <dbl>
 1 京都府 JR奈良線                   3.33
 2 京都府 JR小浜線                  16.5 
 3 京都府 JR山陰本線(京都-米子)    8.67
 4 京都府 JR東海道本線(米原-神戸) 16.3 
 5 京都府 JR片町線〔学研都市線〕     9.4 
 6 京都府 JR舞鶴線                  19   
 7 京都府 京福北野線                 8.36
 8 京都府 京福嵐山本線               6.5 
 9 京都府 京福電気鉄道嵐山本線       6   
10 京都府 京都丹後鉄道宮福線         7.44
# ℹ 499 more rows

最初はPrefLineでグループ化しましたが、summarise()の後、Lineがグループ化変数から外されました。つまり、引数が"drop_last"になっていることです。

それでは、平均値に加えて、第一四分位点と第三四分位点も計算し、Quantileという名で格納してみましょう。

df |>
  filter(!is.na(Walk)) |>
  group_by(Pref, Line) |>
  summarise(Mean     = mean(Walk),
            Quantile = quantile(Walk, c(0.25, 0.75)))
Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
dplyr 1.1.0.
ℹ Please use `reframe()` instead.
ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
  always returns an ungrouped data frame and adjust accordingly.
`summarise()` has grouped output by 'Pref', 'Line'. You can override using the
`.groups` argument.
# A tibble: 1,018 × 4
# Groups:   Pref, Line [509]
   Pref   Line                       Mean Quantile
   <chr>  <chr>                     <dbl>    <dbl>
 1 京都府 JR奈良線                   3.33     2   
 2 京都府 JR奈良線                   3.33     5   
 3 京都府 JR小浜線                  16.5      9.75
 4 京都府 JR小浜線                  16.5     23.2 
 5 京都府 JR山陰本線(京都-米子)    8.67     2   
 6 京都府 JR山陰本線(京都-米子)    8.67    15   
 7 京都府 JR東海道本線(米原-神戸) 16.3     16   
 8 京都府 JR東海道本線(米原-神戸) 16.3     17   
 9 京都府 JR片町線〔学研都市線〕     9.4      2   
10 京都府 JR片町線〔学研都市線〕     9.4      9   
# ℹ 1,008 more rows

同じPrefLineのケースが2つずつ出来ています。最初に来る数値は第一四分位点、次に来るのが第三四分位点です。そして最初のグループ化変数であったPrefLineが、summarise()後もグループ化変数として残っていることが分かります。

しかし、気になる警告(warning)が表示されますね。

Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
dplyr 1.1.0.
ℹ Please use `reframe()` instead.
ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
  always returns an ungrouped data frame and adjust accordingly.

これは「長さ2以上のベクトルを返す場合、summarise()を使うな!」という意味です。今は警告が出るだけで結果としては問題なく動いてますが、{dplyr}1.1.0以降はreframe()関数の仕様を推奨しています。これはsummarise()reframe()に変えるだけで対応可能です。

df |>
  filter(!is.na(Walk)) |>
  group_by(Pref, Line) |>
  reframe(Mean     = mean(Walk),
          Quantile = quantile(Walk, c(0.25, 0.75)))
# A tibble: 1,018 × 4
   Pref   Line                       Mean Quantile
   <chr>  <chr>                     <dbl>    <dbl>
 1 京都府 JR奈良線                   3.33     2   
 2 京都府 JR奈良線                   3.33     5   
 3 京都府 JR小浜線                  16.5      9.75
 4 京都府 JR小浜線                  16.5     23.2 
 5 京都府 JR山陰本線(京都-米子)    8.67     2   
 6 京都府 JR山陰本線(京都-米子)    8.67    15   
 7 京都府 JR東海道本線(米原-神戸) 16.3     16   
 8 京都府 JR東海道本線(米原-神戸) 16.3     17   
 9 京都府 JR片町線〔学研都市線〕     9.4      2   
10 京都府 JR片町線〔学研都市線〕     9.4      9   
# ℹ 1,008 more rows

reframe()の場合、自動的にグルーピングが解除されます(.groups引数の指定はできません)。長さ2以上のベクトルを返す関数を使用する場合は、なるべくsummarise()のかわりにreframe()を使いましょう。


  1. ただし、~関数名()のような書き方のラムダ式は{purrr}パッケージが提供しているわけではない。↩︎

  2. 今回の例のように記述統計量とケース数を同時に計算する場合はn()を使いますが、ケース数のみを計算する場合はgroup_by()summarise()と組み合わせず、count()関数だけ使うことも可能です。グループ化変数(ここではPref)をcount()の引数として指定すると、Prefの値ごとのケース数が表示されます。↩︎