使用dplyr创建多功能描述表格

5

我正在尝试创建一段简单的代码,可以反复使用(仅需进行最小限度的调整),以便能够打印摘要统计表。

可重现的示例会创建一个表格,显示按组分解的变量 V1 的平均值和标准偏差:

data <- as.data.frame(cbind(1:100, sample(1:2), rnorm(100), rnorm(100)))
names(data) <- c("ID", "Group", "V1", "V2")


library(dplyr)
descriptives <- data %>% group_by(Group) %>%
  summarize(
    Mean = mean(V2)
    , SD = sd(V2)
  )

descriptives

我希望修改这个函数,使其能够计算数据集中所有变量的M和SD。
我想用数据集中所有变量的列表vars替换对V1的调用;在这个例子中,包括V1和V2,但通常我有100多个变量。
我希望它能够以这种方式工作,这样我就可以轻松地进行以下操作:
vars <- names(data[3:4])

我希望能够快速选择要求摘要统计信息的列。

愿望清单中有几个要点:

对于给定变量,M和SD应相邻,并且我想在每对上方添加一个变量名称的列。

我希望最终产品看起来像这样:

此示例表

我想使用dplyr,但我也可以接受其他选项。 我还想知道如何交换表格的行和列,使得变量在不同的行中,每个组都有一列(或两列,一列用于M,一列用于SD)。 就像这样:

另一个示例表

接近了,但并不完美:

  1. 新的summarise(across())函数有所帮助:
dplyr::group_by(df, Group) %>% 
  dplyr::summarise(dplyr::across(.cols = c(V1, V2), .fns = c(mean, sd)))

但我不知道如何在不创建多个表格和使用 rbind() 堆叠它们的情况下进行扩展。

  1. 我真的很喜欢 table1() 的格式(vignette),但据我所知,我只能通过另一个变量对M/SDs列进行分层。我真的希望可以添加其他分组变量。

2
https://dplyr.tidyverse.org/reference/summarise_all.html - iod
2
你可以使用 data %>% group_by(Group) %>% summarise_at(vars(vars), list(Mean = mean, SD = sd)) - akrun
@iod 但是这样做并没有让 M 和 SD 列紧挨着每个变量。然后我就必须重新排列所有的变量。你知道更快的方法吗? - socialresearcher
@akrun,这也不能将每个变量的M和SD列放在一起。然后我需要重新排列所有变量。虽然我不反对这样做,但当我有几百个变量时,我需要一种更快速地重新排列它们并添加带有变量名称的标题的方法。 - socialresearcher
@socialresearcher,gtsummary包可能会有所帮助!请看下面的回答。 - Mike
显示剩余2条评论
7个回答

4

我曾有一个类似的问题在这里,并且得到了一些真正有用和简单的答案,使用了tidyverse。最终,一个非常强大的方法被制定出来,我将其封装在一个函数中,并经常使用。

library(tidyverse)

baseline_table <- function(data, variables, grouping_var) {
        
        
        data %>% 
                group_by(!!sym(grouping_var)) %>% 
                summarise(
                        across(
                                all_of(variables),
                                ~ paste0(mean(.) %>% round(2), "(±", sd(.) %>% round(2), ")")
                        )
                ) %>% pivot_longer(
                        cols = -grouping_var,
                        names_to = "variable"
                ) %>% pivot_wider(
                        names_from = grouping_var
                )
        
        
        
}

这个函数需要三个 参数,即 数据变量分组变量 - 它们都比较容易理解。

以下是使用 mtcars 进行测试的示例,其中包含 2级别3级别 的分组变量。

baseline_table(
        data = mtcars,
        variables = c("mpg", "hp"),
        grouping_var = "am"
)

# A tibble: 2 x 3
  variable `0`            `1`           
  <chr>    <chr>          <chr>         
1 mpg      17.153.83)   24.396.17)  
2 hp       160.2653.91) 126.8584.06)

baseline_table(
        data = mtcars,
        variables = c("mpg", "hp"),
        grouping_var = "cyl"
)

# A tibble: 2 x 4
  variable `4`           `6`            `8`           
  <chr>    <chr>         <chr>          <chr>         
1 mpg      26.664.51)  19.741.45)   15.12.56)   
2 hp       82.6420.93) 122.2924.26) 209.2150.98)

它可以直接使用,适用于所有的“data”,以下我使用了“iris”。
baseline_table(
        data = iris,
        variables = c("Sepal.Length", "Sepal.Width"),
        grouping_var = "Species"
)

# A tibble: 2 x 4
  variable     setosa      versicolor  virginica  
  <chr>        <chr>       <chr>       <chr>      
1 Sepal.Length 5.01(±0.35) 5.94(±0.52) 6.59(±0.64)
2 Sepal.Width  3.43(±0.38) 2.77(±0.31) 2.97(±0.32)

当然,有些“分组变量”不太适合进行这种操作。即“cyl”,但是它仍然是个好例子。你可以相应地重新编码你的“分组变量”。
baseline_table(
        data = mtcars %>% mutate(cyl = paste(cyl, "Cylinders", sep = " ")),
        variables = c("mpg", "hp"),
        grouping_var = "cyl"
)

# A tibble: 2 x 4
  variable `4 Cylinders` `6 Cylinders`  `8 Cylinders` 
  <chr>    <chr>         <chr>          <chr>         
1 mpg      26.664.51)  19.741.45)   15.12.56)   
2 hp       82.6420.93) 122.2924.26) 209.2150.98)

您还可以修改该函数以包括关于values的描述性字符串,

baseline_table <- function(data, variables, grouping_var) {
        
        # Generate the table; 
        tmpTable <- data %>% 
                group_by(!!sym(grouping_var)) %>% 
                summarise(
                        across(
                                all_of(variables),
                                ~ paste0(mean(.) %>% round(2), "(±", sd(.) %>% round(2), ")")
                        )
                ) %>% pivot_longer(
                        cols = -grouping_var,
                        names_to = "variable"
                ) %>% pivot_wider(
                        names_from = grouping_var
                )
        
        # Generate Descriptives dynamically
        tmpDesc <- tmpTable[1,] %>% mutate(
                across(.fns = ~ paste("Mean (±SD)"))
        ) %>% mutate(
                variable = ""
        )
        
        
        bind_rows(
                tmpDesc,
                tmpTable
        )
        
        
        
}

尽管这个扩展有点笨重,但它仍然非常强大。 输出如下:

# A tibble: 3 x 4
  variable `4 Cylinders` `6 Cylinders`  `8 Cylinders` 
  <chr>    <chr>         <chr>          <chr>         
1 ""       Mean (±SD)    Mean (±SD)     Mean (±SD)    
2 "mpg"    26.664.51)  19.741.45)   15.12.56)   
3 "hp"     82.6420.93) 122.2924.26) 209.2150.98)

更新: 我已经根据评论中提到的需要,重新编写了此函数以增加灵活性。

library(tidyverse)

baseline_table <- function(data, variables, grouping_var) {

        data %>% 
                group_by(!!!syms(grouping_var)) %>% 
                summarise(
                        across(
                                all_of(variables),
                                ~ paste0(mean(.) %>% round(2), "(±", sd(.) %>% round(2), ")")
                        )
                ) %>% unite(
                        "grouping",
                        all_of(grouping_var)
                ) %>% pivot_longer(
                        cols = -"grouping",
                        names_to = "variables"
                ) %>% pivot_wider(
                        names_from = "grouping"
                )
}

它的工作方式和输出结果是相同的,除非有不止一个grouping_var
baseline_table(
        mtcars,
        variables = c("hp", "mpg"),
        grouping_var = c("am", "cyl")
)

# A tibble: 2 x 7
  variables `0_4`         `0_6`         `0_8`          `1_4`         `1_6`          `1_8`       
  <chr>     <chr>         <chr>         <chr>          <chr>         <chr>          <chr>       
1 hp        84.6719.66) 115.259.18) 194.1733.36) 81.8822.66) 131.6737.53) 299.550.2)
2 mpg       22.91.45)   19.121.63)  15.052.77)   28.084.48)  20.570.75)   15.40.57) 

在更新的函数中,我使用了默认分离器联合。显然,您可以根据需要修改这个函数,使得colnames显示为例如4 Cylinder (Automatic)6 Cylinder (Automatic)等。


所以谢谢,得到的函数很好而且简单。但是我不知道如何扩展它并添加更多的分组变量? - socialresearcher
我错过了那部分。它可以相对简单地完成!我到办公室后会更新我的答案! - Serkan
答案已更新 - 我想是时候稍微整理一下了! - Serkan

4

在排序时有个限制,但如果我们使用select,则可以按列名的子字符串重新排序。

library(dplyr)
library(stringr)
data %>%
    group_by(Group) %>% 
    summarise_at(vars(vars), list(Mean = mean, SD = sd)) %>% 
    select(Group, order(str_remove(names(.)[-1], "_.*")) + 1)
# A tibble: 2 x 5
#  Group V1_Mean V1_SD  V2_Mean V2_SD
#  <dbl>   <dbl> <dbl>    <dbl> <dbl>
#1     1   0.165 0.915  0.146   1.16 
#2     2   0.308 1.31  -0.00711 0.854

3

除了dplyr之外,您可以使用tables,该包允许从表格公式中创建汇总统计信息:

library(tables)

vars <- c("V1","V2")
vars <- paste(vars, collapse="+")

table <- as.formula(paste("(group = factor(Group)) ~ (", vars ,")*(mean+sd)"))
table
# (group = factor(Group)) ~ (V1 + V2) * (mean + sd)

tables::tabular(table, data = data)
#       V1              V2            
# group mean     sd     mean    sd    
# 1     -0.15759 0.9771  0.1405 1.0697
# 2      0.05084 0.9039 -0.1470 0.9949

3
您的原始代码略有变化,如果您明确表示不需要ID(或已分组的Group)列,而是需要其他所有内容,则可以更简单/灵活地使用across()
data %>%
  group_by(Group) %>%
  summarize(across(-ID, .fns = list(Mean = mean, SD = sd), .names = "{.col}_{.fn}"))

# A tibble: 2 x 5
  Group V1_Mean V1_SD V2_Mean V2_SD
  <dbl>   <dbl> <dbl>   <dbl> <dbl>
1     1 -0.0167 0.979   0.145  1.02
2     2  0.119  1.11   -0.277  1.05

编辑: 如果您想要精确地创建您的(第一个)目标,您可以使用gt包来制作具有列跨度的html表格:

data %>%
  group_by(Group) %>%
  summarize(across(-ID, .fns = list(Mean = mean, SD = sd), .names = "{.col}_{.fn}")) %>%
  gt::gt() %>%
  gt::tab_spanner_delim("_") %>%
  gt::fmt_number(-Group, decimals = 2)

enter image description here

关于你的另一个问题,你可以采用类似以下方式获取组合和转置变化:
data %>%
  group_by(Group) %>%
  summarize(across(-ID, .fns = ~paste0(
    sprintf("%.2f", mean(.x)),
    sprintf(" (%.2f)", sd(.x))))) %>%
  t() %>%
  as.data.frame() 


               V1           V2
Group            1            2
V1    -0.02 (0.98)  0.12 (1.11)
V2     0.15 (1.02) -0.28 (1.05)

2

制作漂亮的汇总表的一种方法是使用一个叫做gtsummary的包(注意,我是这个包的共同作者)。下面我只是稍微格式化了一下data2中的数据,并删除了ID变量。然后,只需要两行代码就可以使用gtsummary对数据进行汇总。by语句是将表分层的关键,而在统计输入中,我只是告诉它显示平均值和标准差,默认情况下,gtsummary会显示中位数q1-q3。此表格可以在所有的markdown选项(word、pdf、html)中呈现。

library(dplyr)
library(gtsummary)
data <- as.data.frame(cbind(1:100, sample(1:2), rnorm(100), rnorm(100)))
names(data) <- c("ID", "Group", "V1", "V2")

data2 <- data %>% 
          mutate(Group = ifelse(Group == 1, "Group Var1","Group Var2")) %>%
          select(-ID)

tbl_summary(data2, by = Group,
            statistic = all_continuous()~ "{mean} ({sd})")

outputtable

如果你想要多个分层,但又不想使用tbl_strata,你可以将两个变量合并成一列,并在by语句中使用它。你可以使用unite()合并尽可能多的变量(尽管可能不推荐这样做)。

trial %>%
  tidyr::unite(col = "trt_grade", trt, grade, sep = ", ") %>% 
    select(age, marker,stage,trt_grade) %>%
    tbl_summary(by = c(trt_grade))

1
谢谢。我也喜欢这个表格,tbl_summary的基础代码非常简单。但是如果有多个分组变量和tbl_strata(),扩展起来并不容易,很快就会变得复杂。我有什么遗漏吗? - socialresearcher
@socialresearcher 如果你想要升级而不使用 tbl_strata(),你可以将变量粘贴在一起或使用tidyr将它们合并,然后在by语句中使用这个新变量。我已经编辑了我的答案,提供了一个更新的解决方案。 - Mike

1
一个 data.table 选项
dcast(
  setDT(data)[,
    c(
      .(Meas = c("M", "Sd")),
      lapply(.SD, function(x) c(mean(x), sd(x)))
    ),
    Group,
    .SDcols = patterns("V\\d")
  ], Group ~ Meas,
  value.var = c("V1", "V2")
)

提供

   Group       V1_M    V1_Sd        V2_M     V2_Sd
1:     1 -0.2392583 1.097343 -0.08048455 0.7851212
2:     2  0.1059716 1.011769 -0.23356373 0.9927975

我在这个比赛中支持你 :) - Anoushiravan R
1
@AnoushiravanR 谢谢你啊!我在这里只是为了好玩 :P - ThomasIsCoding

1
您也可以使用基本的R语言:

# using do.call to make the result a data.frame
do.call(
       data.frame
        # here you aggregate for all the functions you need
      ,(aggregate(. ~ Group, data = data[,-1], FUN = function(x) c(mn = mean(x), sd = sd(x))))
       )

这会导致类似于这样的结果:
 Group      V1.mn    V1.sd      V2.mn    V2.sd
1     1  0.1239868 1.008214 0.07215481 1.026059
2     2 -0.2324611 1.048230 0.11348897 1.071467

如果您想要一个更加精美的表格,kableExtra 可以提供很大的帮助。请注意,在 kableExtra 中也需要导入 %>%,但是在 R 4.1 中,您可以使用 |> 替代它:
library(kableExtra)
# data manipulation as above, note the [,-1] to remove the Group column
do.call(
        data.frame
       ,(aggregate(. ~ Group, data = data[,-1], FUN = function(x) c(mn = mean(x), sd = sd(x)))))[,-1] %>%
  # here you define as a kable, and give the names you want to columns
  kbl(col.names = rep(c('mean','sd'),2)  ) %>%
  # some formatting
  kable_paper() %>%
  # adding the first header
  add_header_above(c( "Group 1" = 2, "Group 2" = 2)) %>%
  # another header if you need it
  add_header_above(c( "Big group" = 4)) 

enter image description here

你可以找到更多关于制作优秀表格的内容。


在这种情况下,您还可以尝试类似于以下内容的东西:

do.call(data.frame,
        aggregate(. ~ Group, data = data[,-1], FUN = function(x) paste0(round(mean(x),2),' (', round(sd(x),2),')'))
        ) %>%
 kbl() %>%
 kable_paper()

这导致: 在此输入图片描述

1
kableExtra 是一个非常棒的包。只是从未有机会深入了解它的内部细节。 - Anoushiravan R
1
@Anoushiravan R,你是对的,我也这么认为! - s__

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接