r-wakalangで聞いたら、Rつよつよな人たちが優しく教えてくれました。
無料版slackだと消えてしまうので備忘録も兼ねて記事にしておきます。
問題
大分類/中分類/小分類とあるようなデータに対して、それぞれのレベルに応じて小計を出す方法
想定コードとしては以下。
library(tidyverse)
df <- tibble(Catergory=sample(c("A","B"),size=100,replace = TRUE),
Subcatergory=sample(c("i","j","k"),size=100,replace = TRUE),
Subsubcatergory = sample(c("p","q","r"),size=100,replace = TRUE),
score=rnorm(100, mean=70, sd=10))
こんな感じのデータです。
Catergory | Subcatergory | Subsubcatergory | score |
---|---|---|---|
B | i | r | 68.14897 |
A | k | p | 77.29207 |
B | k | q | 55.99673 |
B | j | q | 77.63559 |
B | j | q | 61.85481 |
B | j | q | 47.65746 |
A | j | q | 74.75191 |
A | i | r | 73.32049 |
とりあえず当座の解決案は下記
df %>%
group_by(Catergory,Subcatergory,Subsubcatergory) %>%
summarise(n=n(),Mean=mean(score),Max=max(score),min=min(score)) %>% ungroup()%>% ungroup() ->df1
df %>%
group_by(Catergory,Subcatergory) %>%
summarise(n=n(),Mean=mean(score),Max=max(score),min=min(score)) %>% ungroup() %>%
mutate(Subsubcatergory="ALL")->df2
df %>%
group_by(Catergory) %>%
summarise(n=n(),Mean=mean(score),Max=max(score),min=min(score)) %>%
mutate(Subsubcatergory="ALL",
Subcatergory="ALL")->df3
bind_rows(df1,df2,df3) %>% arrange(Catergory,Subcatergory,Subsubcatergory)
Catergory | Subcatergory | Subsubcatergory | n | Mean | Max | min |
---|---|---|---|---|---|---|
A | ALL | ALL | 47 | 71.54586 | 101.44371 | 50.75666 |
A | i | ALL | 12 | 68.54373 | 78.54747 | 53.35708 |
A | i | p | 3 | 59.31068 | 66.20233 | 53.35708 |
A | i | q | 5 | 72.53580 | 78.54747 | 69.09217 |
A | i | r | 4 | 70.47843 | 73.32049 | 67.08316 |
A | j | ALL | 23 | 74.57838 | 101.44371 | 58.39143 |
A | j | p | 10 | 77.79374 | 93.15439 | 68.86340 |
A | j | q | 6 | 70.63459 | 82.79089 | 62.69836 |
A | j | r | 7 | 73.36538 | 101.44371 | 58.39143 |
A | k | ALL | 12 | 68.73566 | 84.06569 | 50.75666 |
A | k | p | 4 | 70.71224 | 79.92134 | 56.51757 |
A | k | q | 3 | 64.96890 | 84.06569 | 50.75666 |
A | k | r | 5 | 69.41446 | 73.61545 | 59.87375 |
B | ALL | ALL | 53 | 70.06481 | 88.23096 | 47.65746 |
解法
解法1
KDさんの解法
コードは短くならないのですが、個人的に二通りのやり方を思いつきました。
- まずグループ化変数が異なるだけで、出したい小計は同じなのでそこを関数にしてしまった方がスッキリすると思います。
- 今は分類が3段階なので、段階の数だけ my_sum 関数を呼べばよいですが、もっと階層が深い場合は、再帰的に関数を適用するというのも手かなと思いました。
# 1. 小計を出す関数を定義する ------------------------------
my_sum <- function(x, y, ...) {
x |>
group_by(...) |>
summarize(n = n(), Mean = mean({{ y }}), Max = max({{ y }}), Min = min({{ y }}), .groups = "drop")
}
bind_rows(
my_sum(df, score, Category),
my_sum(df, score, Category, Subcategory),
my_sum(df, score, Category, Subcategory, Subsubcategory)
) |>
mutate(across(ends_with("category"), ~ replace_na(.x, "ALL"))) |>
relocate(ends_with("category"), .before = 1L) |>
arrange(Category, Subcategory, Subsubcategory)
# 2. グループ化変数を操作しながら再帰的に関数を適用する ------------------------------
recursive_my_sum <- function(x, y, ...) {
g <- enquos(...) # グループ化引数 (例: Category, Subcategory, Subsubcategory)
new_g <- head(g, -1) # 次のグループ化引数 (例: Category, Subcategory)
if (length(g) > 0) {
bind_rows(my_sum(x, {{ y }}, !!!g), recursive_my_sum(x, {{ y }}, !!!new_g))
}
}
recursive_my_sum(df, score, Category, Subcategory, Subsubcategory) |>
mutate(across(ends_with("category"), ~ replace_na(.x, "ALL"))) |>
arrange(Category, Subcategory, Subsubcategory)
解法2
fmsan51さんの解法
df_a <- df %>% mutate(Subsubcatergory = "ALL")
df_b <- df_a %>% mutate(Subcatergory = "ALL")
bind_rows(df, df_a, df_b) %>%
group_by(Catergory,Subcatergory,Subsubcatergory) %>%
summarise(n=n(),Mean=mean(score),Max=max(score),min=min(score))
雑感
関数を作ったり、別テーブルを用意したりする方法がありますね。どちらも集約をひとまとめに出来ていて、変更しやすさが高いですね。