11  データハンドリング

ここでは、いわゆる前処理とよばれる、分析に入る前に、データを整える作業について学びます。 分析前の前処理をどれだけ丁寧に行うかによって、分析が容易になったり、結果が変わったりします。

目標としては、

  1. 外部のデータベースなどから入手したcsvファイルやExcelファイルをRに読み込み、
  2. データの構造を理解し、
  3. データを整理・加工して、

記述統計量を計算したり、グラフを描いたりするのに適したデータを作成することです。 このあたりを学習すれば,MS ExcelよりもRの方がデータを分析するのに向いていることがわかると思います。 今後使うパッケージは,tidyverseパッケージです。 ついでに,knitrkableExtraパッケージも読み込んでおきます。

Warning: パッケージ 'stringr' はバージョン 4.2.3 の R の下で造られました
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.3     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.4.4     ✔ tibble    3.2.1
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

 次のパッケージを付け加えます: 'kableExtra' 

 以下のオブジェクトは 'package:dplyr' からマスクされています:

    group_rows

11.1 ここで用いる基本関数

  • head(): データの先頭を表示する関数
  • str(): データの構造を表示する関数

ここで用いるtidyverse関数

11.2 PCを立ち上げる

まずは,VS CodeやRStudioを起動します。

  • VS Codeの場合
    1. フォルダを開く
    2. 編集したいファイルを開く
    3. library()で必要なパッケージを読み込む
    4. csvファイルを読み込む
    5. 作業開始
  • Rstudioの場合
    1. プロジェクトを開く
    2. 編集したいファイルを開く
    3. library()で必要なパッケージを読み込む
    4. csvファイルを読み込む
    5. 作業開始

となります。

11.3 データの読み込む

最初にやることは、データを読み込むことです。 データを読み込むには,tidyversereadrパッケージのread_csv()関数を使います。 関数名のとおりread_csv関数は,csvファイルをRに読み込む関数です。 csvファイルとは,カンマで区切られたテキストファイルで、次のようなデータとなっています。

会社コード,企業名,決算期,決算種別,連結基準,決算月数,上場コード,日経業種コード,売上高,親会社株主に帰属する当期純利益,資産合計,株主資本
0000001,極洋,2006/03,10,1,12,11,235341,152899,2007,65049,14852
0000001,極洋,2007/03,10,1,12,11,235341,157088,2000,66459,16339
0000001,極洋,2008/03,10,1,12,11,235341,147767,1497,57373,16873

csvファイルは,MS Excelで作成することができますが、日経NEEDSからダウンロードしたcsvファイルをExcelで開いてしまうと、勝手にいろいろな処理が勝手に行われてしまうので、注意が必要です。

read_csv

read_csv()関数の引数は非常に多く、設定次第では効率的に前処理ができますが、ここではシンプルに読み込むだけの方法を使います。 読み込んだデータは、dfという名前のオブジェクトに代入します。

df <- read_csv("https://so-ichi.com/presemi_part_two.csv")
Rows: 44527 Columns: 12
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): 会社コード, 企業名, 決算期
dbl (9): 決算種別, 連結基準, 決算月数, 上場コード, 日経業種コード, 売上高, 親会社株主に帰属する当期純利益, 資産合計, 株主資本...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

結果の1行目にRows: 44527 Columns: 12とあるように、dfには44527行12列のデータが読み込まれています。

11.4 データを確認する

このdfのクラスを確認してみます。

class(df)
[1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame" 

すると、"spec_tbl_df""tbl_df""tbl""data.frame"という方が与えられていることがわかります。 ここでは、data.frameであるということだけ覚えておいてください。

read_csv()関数の引数には、次のようなものがあります。

  • file: 読み込むファイルの名前、あるいはパス
  • col_names: 列名を指定するかどうか
  • col_types: 列の型を指定するかどうか
  • locale: ファイルのエンコーディングを指定するかどうか
  • na: 欠損値の文字列を指定するかどうか

読み込んだデータを簡単にチェックする方法として、head()関数があります。 これは、データの先頭の行を表示する関数です。 デフォルトでは、先頭の6行が表示されます。引数nで表示する行数を指定することができます。

head(df, n = 4)
会社コード 企業名 決算期 決算種別 連結基準 決算月数 上場コード 日経業種コード 売上高 親会社株主に帰属する当期純利益 資産合計 株主資本
0000001 極洋 2006/03 10 1 12 11 235341 152899 2007 65049 14852
0000001 極洋 2007/03 10 1 12 11 235341 157088 2000 66459 16339
0000001 極洋 2008/03 10 1 12 11 235341 147767 1497 57373 16873
0000001 極洋 2009/03 10 1 12 11 235341 147554 1587 61184 17839

より詳細な構造を理解するためには、str()関数やdplyr::glimpse()関数を使います。 基本関数str()だと、

str(df)
spc_tbl_ [44,527 × 12] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ 会社コード                    : chr [1:44527] "0000001" "0000001" "0000001" "0000001" ...
 $ 企業名                        : chr [1:44527] "極洋" "極洋" "極洋" "極洋" ...
 $ 決算期                        : chr [1:44527] "2006/03" "2007/03" "2008/03" "2009/03" ...
 $ 決算種別                      : num [1:44527] 10 10 10 10 10 10 10 10 10 10 ...
 $ 連結基準                      : num [1:44527] 1 1 1 1 1 1 1 1 1 1 ...
 $ 決算月数                      : num [1:44527] 12 12 12 12 12 12 12 12 12 12 ...
 $ 上場コード                    : num [1:44527] 11 11 11 11 11 11 11 11 11 11 ...
 $ 日経業種コード                : num [1:44527] 235341 235341 235341 235341 235341 ...
 $ 売上高                        : num [1:44527] 152899 157088 147767 147554 145778 ...
 $ 親会社株主に帰属する当期純利益: num [1:44527] 2007 2000 1497 1587 1086 ...
 $ 資産合計                      : num [1:44527] 65049 66459 57373 61184 64301 ...
 $ 株主資本                      : num [1:44527] 14852 16339 16873 17839 18390 ...
 - attr(*, "spec")=
  .. cols(
  ..   会社コード = col_character(),
  ..   企業名 = col_character(),
  ..   決算期 = col_character(),
  ..   決算種別 = col_double(),
  ..   連結基準 = col_double(),
  ..   決算月数 = col_double(),
  ..   上場コード = col_double(),
  ..   日経業種コード = col_double(),
  ..   売上高 = col_double(),
  ..   親会社株主に帰属する当期純利益 = col_double(),
  ..   資産合計 = col_double(),
  ..   株主資本 = col_double()
  .. )
 - attr(*, "problems")=<externalptr> 

のように、変数ごとにデータの型、データの個数、データがいくつか表示されます。 glimpse()関数も同様に、変数ごとにデータの型やデータのいくつかが表示されます。

Rows: 44,527
Columns: 12
$ 会社コード                     <chr> "0000001", "0000001", "0000001", "00000…
$ 企業名                         <chr> "極洋", "極洋", "極洋", "極洋", "極洋",…
$ 決算期                         <chr> "2006/03", "2007/03", "2008/03", "2009/…
$ 決算種別                       <dbl> 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,…
$ 連結基準                       <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ 決算月数                       <dbl> 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,…
$ 上場コード                     <dbl> 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,…
$ 日経業種コード                 <dbl> 235341, 235341, 235341, 235341, 235341,…
$ 売上高                         <dbl> 152899, 157088, 147767, 147554, 145778,…
$ 親会社株主に帰属する当期純利益 <dbl> 2007, 2000, 1497, 1587, 1086, 58, 423, …
$ 資産合計                       <dbl> 65049, 66459, 57373, 61184, 64301, 7692…
$ 株主資本                       <dbl> 14852, 16339, 16873, 17839, 18390, 1778…

これで、変数の名前やタイプ、どんなデータが入っているのかをサクッと確認しておきましょう。

11.5 データの整理・加工する

データの整理・加工には、dplyrパッケージの関数を使います。 dplyrパッケージは、データの整理・加工に特化したパッケージで、tidyverseパッケージに含まれています。 ここからは,基本的にパイプ演算子を使います。 dplyrパッケージの関数は,パイプ演算子を使って処理をつなげることで,データの整理・加工の処理が分かりやすくなります。

11.5.1 select()関数

select()関数は、データの変数を選択する関数です。

select関数

引数には、選択したい変数名を直接していするか、以下の関数をつかって変数名の一部を指定することができます。

複数の変数を具体的に指定する場合は,c()で変数名を指定します。

先ほど読み込んだdfから,企業名,決算期,売上高の3つの変数を選択する場合は,次のように書きます。 読み込んだデータをdf_saleというオブジェクトに代入します。 基本的に,読み込んだオリジナルのデータは加工せず,加工したデータフレームは新しいオブジェクトに代入していきます。

df_sale <- df |>
    select(企業名, 決算期, 売上高, 資産合計)

これで,4変数からなるdf_saleが作成されました。

11.5.2 filter()関数

filter()関数は,データの行(つまり観測値)を抽出する関数です。

filter関数

filter()関数の引数は,行を抽出する条件式です。 データフレームの章で学習したものと同様に,以下のような条件を指定することで,必要とされる行のみを抽出できます。

  • ==!= : 等しいかそれ以外か
  • >>=<<= : 大小関係
  • %in% : ベクトルの中に含まれるかどうか
  • is.na() : 欠損値かどうか

また条件式を複数指定することもできます。

  • &はAND つまり「かつ」
  • |はOR つまり「あるいは」

これらの条件式を組み合わせることで、複雑な条件を指定することができます。 例えば,先ほど作成したdf_saleから,トヨタ自動車で,かつ売上高が30兆円以上のデータを抽出する場合,

df_sale_toyota <- df_sale |>
    filter(企業名 == "トヨタ自動車" & 売上高 >= 3e7)
df_sale_toyota
企業名 決算期 売上高 資産合計
トヨタ自動車 2019/03 30225681 51936949
トヨタ自動車 2022/03 31379507 67688771

のように書きます。 財務データは単位が100万円なので,30兆円は30000000となりますが,ゼロが多すぎて一見何円か分からないので,指数表記の3e7としています。 これは3 \times 10^7という意味で,3の後ろにゼロが7こあるという意味です。

filter()関数を使った結果,売上高が30兆円以上だった年度が2019年度と2022年度だったことがわかりました。

11.5.3 mutate()関数

mutate()関数は、データに新しい変数を追加する関数です。

mutate関数

すでにある変数を使って新しい変数を作成したり、変数の値を変換したり、新規の変数を追加したりすることができます。

mutate()関数の引数は、

  • 新しい変数名 = 変数の値を計算する式

となります。 たとえば,先ほど作成したdf_sale_toyotaのデータを用いて,総資産回転率を計算する場合は,次のように書きます。

総資産回転率は,売上高を資産合計で除したものです。

df_sale_toyota <- df_sale_toyota |>
    mutate( # 変数の作成
        総資産回転率 = round(売上高 / 資産合計, digits = 3) # 計算式
        )
df_sale_toyota
企業名 決算期 売上高 資産合計 総資産回転率
トヨタ自動車 2019/03 30225681 51936949 0.58
トヨタ自動車 2022/03 31379507 67688771 0.46

mutate()関数には次のようなオプションも用意されています。

  • .before.after : 変数を追加する位置を指定
    • .before = 売上高 : 売上高の前に追加
    • .after = 売上高 : 売上高の後に追加
  • .keep : 新しい変数を追加する際に、元の変数を残すかどうか
    • keep = "all" : 元の変数を残す
    • keep = "used": 使った変数だけ残す
    • keep = "unused": 変数の作成に使った変数を除外する
    • keep = "none": 新変数のみを残す

これは見た方が早いので、mutate()のヘルプにある例で確認してみましょう。 次のような1行2列のデータフーレムを用意します。

コード

df_ex <- data.frame(x = 1, y = 2)
df_ex

結果

x y
1 2

11.5.3.1 mutate()のデフォルト動作

まずはデフォルトの動作を確認します。

コード

df_ex |> mutate(z = x + y)

結果

x y z
1 2 3

一番右の列にzが追加されました。これがデフォルトの動作となります。

11.5.3.2 .before.after

次に、.before = 1を指定してみます。

コード

df_ex |>
    mutate(z = x + y, .before = 1) # 1の前に3が追加

結果

z x y
3 1 2

1の前に新変数zが追加されました。 次に、.after = xを指定してみます。

コード

df_ex |>
    mutate(z = x + y, .after = x) # xの後に3が追加

結果

x z y
1 3 2

xの後に新変数zが追加されました。 このように、.before.afterを使うことで、新変数を追加する位置を指定することができます。

11.5.3.3 .keep

最後に、.keepを指定してみます。 確認ように,次のような1行4列のデータフーレムを用意します。

コード

df_ex <- data.frame( # x,y,a,bの4変数
    x = 1,
    y = 2,
    a = "a",
    b = "b"
    )
df_ex

結果

x y a b
1 2 a b

こちらもデフォルトの動作から確認していきます。

コード

df_ex |>
    mutate(z = x + y, .keep = "all") # デフォルトの動作

結果

x y a b z
1 2 a b 3

新変数zが一番右に追加され、もともとの変数もすべて残っています。 これがデフォルトの動作となります。

次に、.keep = "used"を指定してみます。

コード

df_ex |>
    mutate(z = x + y, .keep = "used") # 計算に使わなかった変数が除外

結果

x y z
1 2 3

新変数zとそれを作成するために使った変数xyが残り、 計算に使われなかった変数abが除外されています。 新変数作成に用いられない変数を一括で除外したいときは、.keep = "used"を指定します。

次に、.keep = "unused"を指定してみます。

コード

df_ex |>
    mutate(z = x + y, .keep = "unused") # 計算に使わなかった変数が除外

結果

a b z
a b 3

新変数zを作成するために使ったxyが除外されています。 新しい変数を作成したあとに、計算の元になった変数を使わない場合は、.keep = "unused"を指定するとよいでしょう。

最後に、.keep = "none"を指定してみます。

コード

df_ex |>
    mutate(z = x + y, .keep = "none") # 計算に使わなかった変数が除外

結果

z
3

.keep = "none"を指定すると、mutate()で新たに作成した変数のみが残り、元の変数はすべて除外されます。 新変数以外は必要ない、という場合は.keep = "none"を指定するとよいでしょう。

11.5.4 group_by()summarise()

11.5.4.1 group_by()関数

group_by()関数は、データをグループ化する関数です。 ある変数がカテゴリー変数となっている場合に,そのカテゴリーごとにデータ処理を行うために使います。 基本的には,group_by()関数でグループ化した後に,summarise()関数で集約することになります。

group_byとsummarise

group_by()関数の引数は,グループ化する変数名です。

  • group_by(グループ化する変数名)

オプションとして,.add.dropを指定することができます。 デフォルトでは,.add = FALSE.drop = TRUEとなっています。 .addは,既存のグループに新しいグループを追加するかどうかを指定します。 .dropは,既存のグループを削除するかどうかを指定します。

11.5.4.2 summarise()関数

summarise()関数の引数は,集約する変数名です。 mutate()関数と同じように新しい変数を作成するのですが,mutate()関数と違って,集約された値が作成されます。 そのため,summarise()関数は,mutate()関数と違って,データの行数が減ることに注意してください。

summarise()関数の引数は,

  • 新しい変数名 = 集約関数(集約する変数名)

となります。 集約関数には,mean()median()sd()などの統計量を計算する関数を指定します。 もちろん,trimna.rmなどのオプションも指定することができます。

また,summarise()関数のオプションとして,.groupsを指定することができます。 .groupsは,集約後のデータの行数を指定します。

  • .groups = "drop" : 集約後のデータの行数を指定しない
  • .groups = "keep" : 集約後のデータの行数を指定する
  • .groups = "rows" : 集約後のデータの行数を指定する
  • .groups = "drop_last" : 集約後のデータの行数を指定する
  • .groups = "keep_last" : 集約後のデータの行数を指定する
  • .groups = "rows_last" : 集約後のデータの行数を指定する
  • .groups = "drop_first" : 集約後のデータの行数を指定する

これも具体例を見ながら確認してみましょう。

11.5.4.3 group_by()summarise()の具体例

上で作成したdfを用いて,産業別の売上高平均を表す表を作成してみましょう。 手順としては,

  1. 産業中分類を表す変数を作成し,
  2. 産業中分類ごとにグループ化し,
  3. グループごとに平均売上高を計算し,
  4. 平均売上高を大きい順(降順)に並び替える,

データフレームを特定の変数の値に基づいて並び替えるには,arrange()関数を使います。 arrange()関数はデフォルトで昇順に並び替えるので,降順に並び替える場合は,desc()関数を使います。 したがって,ここでは,arrange(desc(平均売上高))となります。

となります。

日経業種コードは,6ケタの数値で出来ており,最初の1ケタは大分類,次の2ケタは中分類,最後の3ケタが小分類を表しています。 中分類を表すカテゴリー変数を作りたいので,substr()関数を使って,日経業種コードの2ケタ目と3ケタ目を抽出します。 substr(文字列,開始位置,終了位置)という関数で,文字列の指定した位置の文字を抽出することができます。

df |>
    mutate( # substr()で産業中分類を作成
        産業中分類 = substr(日経業種コード, 2, 3)
        ) |>
    group_by(産業中分類) |> # 産業中分類ごとにグループ化
    summarise( # 平均売上高を計算
        平均売上高 = mean(売上高, na.rm = TRUE) # 平均売上高を計算
        ) |>
    arrange(desc(平均売上高)) |> # 平均売上高で降順に並び替え
    head(5) |> # 上位5社を表示
    kable(format.args = list(big.mark = ",")) |> # 3桁ごとにカンマを表示
    kable_styling(full_width = FALSE)  # 表の幅を自動調整
産業中分類 平均売上高
11 1,635,554.6
67 1,544,265.2
27 1,117,692.0
65 1,048,226.6
61 733,240.6

日経産業中分類の11は石油,67は電力,27は自動車・自動車部品,65は通信,61は空輸を表しています。

各種オプションの効果を確認するために,group_by()summarise()のヘルプにある例を確認してみましょう。

11.5.4.4 group_by()のオプション

練習用データmtcarsを用いて,group_by()のオプションを確認してみましょう。

mtcarsは32行11列のデータフレームであり,変数として,"mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb"をもっています。 mpgは燃費,cylはシリンダー数,dispは排気量,hpは馬力,dratは変速機のギア比,wtは重量,qsecは1/4マイル走のタイム,vsはV型エンジンか直列エンジンか,amは変速機が自動か手動か,gearは変速機のギア数,carbはキャブレターの数を表しています。

シリンダー数でグループ化したものをby_cylというオブジェクトに代入します。

by_cyl <- mtcars |> group_by(cyl)
glimpse(by_cyl)
Rows: 32
Columns: 11
Groups: cyl [3]
$ mpg  <dbl> 21.0, 21.0, 22.8, 21.4, 18.7, 18.1, 14.3, 24.4, 22.8, 19.2, 17.8,…
$ cyl  <dbl> 6, 6, 4, 6, 8, 6, 8, 4, 4, 6, 6, 8, 8, 8, 8, 8, 8, 4, 4, 4, 4, 8,…
$ disp <dbl> 160.0, 160.0, 108.0, 258.0, 360.0, 225.0, 360.0, 146.7, 140.8, 16…
$ hp   <dbl> 110, 110, 93, 110, 175, 105, 245, 62, 95, 123, 123, 180, 180, 180…
$ drat <dbl> 3.90, 3.90, 3.85, 3.08, 3.15, 2.76, 3.21, 3.69, 3.92, 3.92, 3.92,…
$ wt   <dbl> 2.620, 2.875, 2.320, 3.215, 3.440, 3.460, 3.570, 3.190, 3.150, 3.…
$ qsec <dbl> 16.46, 17.02, 18.61, 19.44, 17.02, 20.22, 15.84, 20.00, 22.90, 18…
$ vs   <dbl> 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0,…
$ am   <dbl> 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,…
$ gear <dbl> 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 4, 4, 4, 3, 3,…
$ carb <dbl> 4, 4, 1, 1, 2, 1, 4, 2, 2, 4, 4, 3, 3, 3, 4, 4, 4, 1, 2, 1, 1, 2,…

出力の3行目にGroups: cyl [3]とあるように,cylでグループ化されていることがわかります。 このグループごとに,排気量dispと馬力hpの平均を計算してみましょう。

by_cyl |> summarise(
  disp = mean(disp),
  hp = mean(hp)
)
cyl disp hp
4 105.14 82.64
6 183.31 122.29
8 353.10 209.21

3グループごとに2変数の平均を計算したので,3行3列のデータフレームが作成されました。 次に,max()関数を使ってグループ内で最大の排気量を持つデータを抽出してみましょう。

by_cyl |>
  filter(
    disp == max(disp) # 最大の排気量を持つデータを抽出
    ) |> kable()
mpg cyl disp hp drat wt qsec vs am gear carb
21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1
24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2
10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4

3つのグループ内で最大のdispをもつ行がfilter()により抽出され,データフレームが3行になっていことが分かります。

グルーピング化を解除する効果について確認してみましょう。 次のように,vsamでグループ化したby_vs_amを作成し,n()を使ってグループごとのデータ数を計算します。

by_vs_am <- mtcars |>
    group_by(vs, am) # vsとamでグループ化
by_vs <- by_vs_am |>
    summarise(n = n()) # グループ内のデータ数を計算
`summarise()` has grouped output by 'vs'. You can override using the `.groups`
argument.

英語のメッセージが出力されました。 メッセージの内容は,「summarise()関数は,vsでグループ化されたデータを出力しているので,グループ化を上書きしたいなら,.groups引数を使え」ということです。

作成したby_vsの中を確認してみましょう。

group_vars(by_vs) # グループ数
[1] "vs"
by_vs |> kable()
vs am n
0 0 12
0 1 6
1 0 7
1 1 7

グループがvsとなっており,amでのグループ化が行われていないことがわかります。 そのため,

by_vs |> summarise(n = sum(n))
vs n
0 18
1 14

とすると,vsごとの合計データ数が集計されています。 グループ化を解除するには,ungroup()関数を使います。

by_vs |>
  ungroup() |> #グループ化を解除
  summarise(n = sum(n)) |>
  kable(table.attr = "style='width:40%;'") |> kable_styling(full_width = FALSE)
n
32

グループが解除されたので,データの個数はデータフレーム全体のデータ数を表します。

mtcars |> nrow()
[1] 32

デフォルトでは,group_by()は既存のグループ化を上書きします。

by_cyl |> # cylでグループ化済み
  group_by(vs, am) |> # vsとamでグループ化
  group_vars() # グループ化されている変数を確認
[1] "vs" "am"

cylでグループ化されていたby_cylが,vsamでグループで上書きされています。 add = TRUEを指定すると,既存のグループ化に新しいグループ化を追加します。

by_cyl |>
  group_by(vs, am, .add = TRUE) |>
  group_vars()
[1] "cyl" "vs"  "am" 

グループがcylvsamの3つになっていることがわかります。

group_by()関数には,式を指定することもできます。 次の例では,vsamの値を足した値でグループ化しています。

mtcars |>
  group_by(vsam = vs + am) |>
  head() |>
  kable() |> kable_styling(font_size = 20) |>
  column_spec(12, bold = TRUE, background = 'mistyrose')
mpg cyl disp hp drat wt qsec vs am gear carb vsam
21.0 6 160 110 3.90 2.620 16.46 0 1 4 4 1
21.0 6 160 110 3.90 2.875 17.02 0 1 4 4 1
22.8 4 108 93 3.85 2.320 18.61 1 1 4 1 2
21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 1
18.7 8 360 175 3.15 3.440 17.02 0 0 3 2 0
18.1 6 225 105 2.76 3.460 20.22 1 0 3 1 1

mutate()はグループ化されていないデータに対して常に実行されるので,行数は変わりません。

次の例では,group_by()hp_cut = cut(hp,3)という式を指定することで,hpを3等分したカテゴリーを表す変数hp_cutを作成しています。

by_hp3 <- mtcars |>
  group_by(vs) |>
  group_by(hp_cut = cut(hp, 3))
by_hp3 |>
  head() |> # 先頭6行のみ表示
  kable() |> # 表形式
  kable_styling(font_size = 20) |> # フォントサイズ
  column_spec(12, bold = TRUE, background = 'mistyrose')
mpg cyl disp hp drat wt qsec vs am gear carb hp_cut
21.0 6 160 110 3.90 2.620 16.46 0 1 4 4 (51.7,146]
21.0 6 160 110 3.90 2.875 17.02 0 1 4 4 (51.7,146]
22.8 4 108 93 3.85 2.320 18.61 1 1 4 1 (51.7,146]
21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 (51.7,146]
18.7 8 360 175 3.15 3.440 17.02 0 0 3 2 (146,241]
18.1 6 225 105 2.76 3.460 20.22 1 0 3 1 (51.7,146]

by_hp3の一番右の列にhp_cutが追加され、(51.7,146](146,241]といったカテゴリーが作成されているのがわかります。 このhp_cutの中に含まれるデータ数をtable()関数で確認してみましょう。

table(by_hp3$hp_cut)

(51.7,146]  (146,241]  (241,335] 
        17         11          4 

by_hp3hp_cutが3つのグループに分けられていることがわかります。

グループごとにmutate()を実行したい場合は,明示的にmutate()を使う必要があります。 たとえば,vsの値(0か1)ごとにhpを3等分したカテゴリーを表す変数hp_cutを作成するには,次のように書きます。

by_hp6 <- mtcars |>
  group_by(vs) |> # vsでグループ化
  mutate(hp_cut = cut(hp, 3)) |> # hpを3等分したカテゴリーを表す変数hp_cutを作成
  group_by(hp_cut)

by_hp6 |>
  head() |>
  kable() |>
  kable_styling(font_size = 20) # hp_cutでグループ化
mpg cyl disp hp drat wt qsec vs am gear carb hp_cut
21.0 6 160 110 3.90 2.620 16.46 0 1 4 4 (90.8,172]
21.0 6 160 110 3.90 2.875 17.02 0 1 4 4 (90.8,172]
22.8 4 108 93 3.85 2.320 18.61 1 1 4 1 (75.7,99.3]
21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 (99.3,123]
18.7 8 360 175 3.15 3.440 17.02 0 0 3 2 (172,254]
18.1 6 225 105 2.76 3.460 20.22 1 0 3 1 (99.3,123]

by_hp6の一番右の列にhp_cutが追加され、(51.7,146](146,241]といったカテゴリーが作成されているのがわかります。 先と同様に、hp_cutの中に含まれるデータ数をtable()関数で確認してみましょう。

table(by_hp6$hp_cut)

 (90.8,172]   (172,254]   (254,335] (51.9,75.7] (75.7,99.3]  (99.3,123] 
          5          11           2           5           3           6 

つぎはvshp_cutでグループ化したため、6グループに分けられていることがわかります。

group_by()関数のオプション.dropは,グループ化されたデータの行数が0になった場合に,グループを削除するかどうかを指定します。 各グループがどの行を含んでいるのかをgroup_rows()で確認します。

tbl <- tibble(
  x = 1:10, # 1から10の数列
  y = factor(
    rep(c("a", "c"), each  = 5), # "a"と"c"を5回繰り返す
    levels = c("a", "b", "c") # レベルには"a"と"b"と"c"を指定
    ) # レベルを指定
)
tbl |> kable() |> kable_styling(font_size = 20)
x y
1 a
2 a
3 a
4 a
5 a
6 c
7 c
8 c
9 c
10 c

tblのカテゴリー数が3となっていますが,カテゴリー変数yにはacしか存在しない,というデータを作りました。 そのカテゴリー変数yのグループごとの行数を確認してみましょう。

ヘルプにあるとおりにgroup_rows()を使うとエラーがでますが、これはgroup_rows()dplyrパッケージにあるものではなく基本関数group_rows()を使っているためです。 したがって,dplyr::group_rows()とすることでエラーを回避できます。

tbl |>
  group_by(y, .drop = FALSE) |>
  dplyr::group_rows()
<list_of<integer>[3]>
[[1]]
[1] 1 2 3 4 5

[[2]]
integer(0)

[[3]]
[1]  6  7  8  9 10

出力がリスト型になっており,[[2]]の結果がinteger(0)となっています。

11.5.5 group_by()mutate()

同じ観測値(行)での変数の計算を行う場合は,mutate()のみで十分ですが,異なる時点の変数の計算を行う場合は注意が必要です。 たとえば,以下のように,3つの企業における3ヶ年の売上高と総資産データから,総資産回転率を計算する場合を考えます。

df <- data.frame(
    name = c(rep("トヨタ",3), rep("ホンダ",3), rep("日産",3)),
    year = rep(2021:2023,3),
    sale = c(272145,313795,371542,131705,145526,169077,78625,84245,105966),
    TotalAsset = c(622671,676887,743031,219210,239731,246700,164520,163724,175985)
    )
df <- df |>
    mutate(
        turnover = sale / lag(TotalAsset)
        )
df |> kable() |> kable_styling(font_size = 20)
name year sale TotalAsset turnover
トヨタ 2021 272145 622671 NA
トヨタ 2022 313795 676887 0.5039499
トヨタ 2023 371542 743031 0.5488981
ホンダ 2021 131705 219210 0.1772537
ホンダ 2022 145526 239731 0.6638657
ホンダ 2023 169077 246700 0.7052780
日産 2021 78625 164520 0.3187069
日産 2022 84245 163724 0.5120654
日産 2023 105966 175985 0.6472234

結果は出力されましたが,turnoverの計算が企業ごとに行われていないことがわかります。

異なる時点の計算

具体的には,ホンダの2021の売上高をトヨタの2023年の総資産で割った値や,日産の2021年の売上高をホンダの2023年の総資産で割った値が計算されてしまっています。 これはdplyr::lag()関数がデフォルトで全体のデータを対象としているため,企業ごとではなく,データフレーム全体での計算が行われているためです。

このような場合は,group_by()関数を使って企業ごとにグループ化し,mutate()関数を使って計算する必要があります。

df <- df |>
    group_by(name) |>
    mutate(
        turnover = sale / lag(TotalAsset)
        )
df |> kable() |> kable_styling(font_size = 20)
name year sale TotalAsset turnover
トヨタ 2021 272145 622671 NA
トヨタ 2022 313795 676887 0.5039499
トヨタ 2023 371542 743031 0.5488981
ホンダ 2021 131705 219210 NA
ホンダ 2022 145526 239731 0.6638657
ホンダ 2023 169077 246700 0.7052780
日産 2021 78625 164520 NA
日産 2022 84245 163724 0.5120654
日産 2023 105966 175985 0.6472234

2021年の総資産回転率の計算のためには,2020年の総資産が必要ですが,2021年からしかデータが無いため,NAが入っていることが分かります。 これで正確な計算ができました。

group_by()summarise()を使うと,グループごとの統計量を計算するため,出力される結果はグループ数と同じ行数になりますが, group_by()mutate()を使うと,グループごとの計算を行うため,出力される結果は元の行数と同じになります。