# setup
library(tidyverse)
library(rio)
library(here)
library(sjPlot)
library(sjmisc)
std_sm <- import(here("files/data/student_sm.dta"))如何制作表格?
为什么要制表?
数据分析过程是对数据进行减化的过程,是把大量的原始数据浓缩成一个或者一组数字,比如频数、均值、相关系数,用这些所谓的统计值来描述研究对象的特征和特征之间的关联。呈现分析结果的形式有两种:表格与图形。表格通过二维方格的形式将统计值直接显示出来。而图形则是通过色彩与形状间接地呈现这些统计值。虽然图形在表达信息方向比较直观,更容易发现多个统计值之间的关联。但是表格在表达统计信息时比较准确和全面,且图形的基础依然是统计值,因此,制表是数据分析过程中一项基本工作。R作为一门数据分析语言,提供了很多便捷的制表工具。
R制表package
表格是用来有条理地呈现统计数据,制表不仅涉及将原始数据统计计算生成统计表之外,还涉及对表格样式的设计与调整。R语言在这两个方面都有许多优秀的功能包。用于生成表格的package有base,stats, rstatix, sjmisc, janitor, gtsummary, modelsummary。base中的table(), summary()可分别对变量进行描述性统计,stats中t.test(), cor(), lm(), glm()可以用来生成相关性的统计值,但这些function的输出结果不是太友好。而第三方package提供很多有效的解决方案。其中gtsummary 和modelsummary专注于制表,gtsummary兼容pipe,对labelled的数据支持比较友好,但其将统计值整合在一个单元格内,后期调整空间较小。相反,modelsummary生成表格的功能及可调整空间都较大,尤其对统计回归表格的支持更为广泛。rstatix, sjmisc, janitor虽然不是专门生成表格的package,但也提供一系列制表的function, 比如sjmisc中的frq(), janitor中的tabyl(). rstatix既有计算频数和统计值的function,比如freq_table()和get_summary_stats(),且具有全面的相关性分析的function,比如t_test(), chisq_test(), cor_test(), anova_test()等,其生成的结果为data.frame格式,可直接用于制表。
对表格进行设计与调整的package有gt, gtExtras, flextable, formattable, kableExtra, DT, reactable. 其中,flextable可对表格全方位调整,语法结构容易理解,输出结果简约工整,适合制作正式出版所需的表格。gt和gtExtras配合使用,也可以对表格全方位调整,表格样式比较适合网站显示,且可以在表格中加入图形等要素。formattable和kableExtra配合使用,也可以对表格进行全方位的调整。DT和reactable主要是可以生成具有交互功能的表格,二者功能具有重合之处。本文主要讲述modelsummary , flextable,部分讲解rstatix, sjmisc, kableExtra, DT.
R制表技术介绍
根据表格统计信息的复杂程度,可将表格分为三种类型:描述性表格,比如频数表、统计表、交叉表等;相关性分析结果表,比如correlation, anovo, t-test, chi2-test的结果表;回归分析结果表。本文主要关注第一类,也会涉及相关性分析表,不探讨回归分析结果表制作的问题。
频数表主要是对离散型变量各取值的频数和占比进行统计。交叉表是两个或以上的离散型变量的频数及占比的统计描述。统计表主要是对连续型变量的均值、方差、区间等信息的统计。还有一种两格就是分组生成连续型变量的统计表。
单一变量的表述
base包有两个快速统计分析的function: table()和summary()。可以有助于快速获得单一变量的统计信息。
#base
table(std_sm$sex)
  1   2 
418 394 #base
summary(std_sm$tr_chn)   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
    2.0    98.0   112.0   106.8   120.0   137.0      42 sjmisc包中的frq()可快速生成、格式友好的频数表。
library(sjmisc)
std_sm %>% 
  frq(computer, out="viewer")| val | label | frq | raw.prc | valid.prc | cum.prc | |
| 0 | 都没有 | 38 | 4.66 | 4.69 | 4.69 | |
| 1 | 有电脑,无网络 | 41 | 5.03 | 5.06 | 9.75 | |
| 2 | 有电脑和网络 | 731 | 89.69 | 90.25 | 100.00 | |
| NA | NA | 5 | 0.61 | NA | NA | |
| total N=815 · valid N=810 · x̄=1.86 · σ=0.47 | ||||||
对连续型变量,rstatix中的get_summary_stats()方便易用。
library(rstatix)
std_sm %>% 
  get_summary_stats(tr_chn)# A tibble: 1 × 13
  variable     n   min   max median    q1    q3   iqr   mad  mean    sd    se
  <chr>    <dbl> <dbl> <dbl>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 tr_chn     773     2   137    112    98   120    22  13.3  107.  17.9 0.643
# … with 1 more variable: ci <dbl>同时描述多个变量
在探索性分析初级阶段,需要首先对单个变量统计分析,在数据分析的下一阶段或者在分析结果展示阶段,会涉及对多个变量同时描述的情况。对于多个变量同时描述,我们将集中使用modelsummary中的function。首先是对多个离散型变量的分析,使用datasummary_skim()。
#modelsummary
library(sjmisc)
std_sm <- std_sm %>%
  mutate(
    across(c(matbias, party_p,ses_p,aftercls,
    sex, hukou, computer, psyroom),to_label)
    )
#datasummary_skim
library(modelsummary)
std_sm %>% 
  select(sex, party_p, computer, aftercls) %>% 
  datasummary_skim(type = "categorical")| N | % | ||
|---|---|---|---|
| sex | 男 | 418 | 51.3 | 
| 女 | 394 | 48.3 | |
| party_p | 共产党员 | 105 | 12.9 | 
| 民主党派 | 10 | 1.2 | |
| 群众 | 664 | 81.5 | |
| computer | 都没有 | 38 | 4.7 | 
| 有电脑,无网络 | 41 | 5.0 | |
| 有电脑和网络 | 731 | 89.7 | |
| aftercls | 否 | 589 | 72.3 | 
| 是 | 225 | 27.6 | 
接下来是对多个连续型变量的同时描述,使用datasummary_skim()。
std_sm %>% 
  select(tr_chn, tr_mat,tr_eng, height, weight, health) %>% 
  datasummary_skim(type = "numeric")| Unique (#) | Missing (%) | Mean | SD | Min | Median | Max | ||
|---|---|---|---|---|---|---|---|---|
| tr_chn | 92 | 5 | 106.8 | 17.9 | 2.0 | 112.0 | 137.0 | |
| tr_mat | 124 | 5 | 111.1 | 30.4 | 3.0 | 121.0 | 150.0 | |
| tr_eng | 183 | 5 | 107.4 | 30.3 | 10.0 | 116.0 | 148.0 | |
| height | 44 | 2 | 167.1 | 7.8 | 145.0 | 166.0 | 190.0 | |
| weight | 110 | 7 | 112.1 | 25.2 | 50.0 | 106.0 | 220.0 | |
| health | 6 | 0 | 3.9 | 0.9 | 1.0 | 4.0 | 5.0 | 
datasummary_balance()可以将多个连续型变量和离散型变量同时显示。
std_sm %>% 
  select(tr_chn, tr_mat,tr_eng, height, weight, health,
                  sex, party_p, computer, aftercls) %>% 
  datasummary_balance(~ 1,data = .)| Mean | Std. Dev. | ||
|---|---|---|---|
| tr_chn | 106.8 | 17.9 | |
| tr_mat | 111.1 | 30.4 | |
| tr_eng | 107.4 | 30.3 | |
| height | 167.1 | 7.8 | |
| weight | 112.1 | 25.2 | |
| health | 3.9 | 0.9 | |
| N | Pct. | ||
| sex | 男 | 418 | 51.3 | 
| 女 | 394 | 48.3 | |
| party_p | 共产党员 | 105 | 12.9 | 
| 民主党派 | 10 | 1.2 | |
| 群众 | 664 | 81.5 | |
| computer | 都没有 | 38 | 4.7 | 
| 有电脑,无网络 | 41 | 5.0 | |
| 有电脑和网络 | 731 | 89.7 | |
| aftercls | 否 | 589 | 72.3 | 
| 是 | 225 | 27.6 | 
datasummary_balance()还可以对变量分组生成描述统计表。
std_sm %>% 
  select(tr_chn, tr_mat,tr_eng, height, weight, health,
        sex, party_p, computer, aftercls) %>% 
  datasummary_balance(~ sex,data = .,dinm_statistic = "p.value")| Mean | Std. Dev. | Mean | Std. Dev. | Diff. in Means | p | ||
|---|---|---|---|---|---|---|---|
| tr_chn | 103.2 | 19.4 | 110.6 | 15.3 | 7.4 | 0.0 | |
| tr_mat | 108.7 | 33.8 | 113.8 | 26.0 | 5.1 | 0.0 | |
| tr_eng | 99.9 | 33.4 | 115.4 | 24.1 | 15.4 | 0.0 | |
| height | 171.9 | 6.8 | 162.1 | 5.1 | -9.8 | 0.0 | |
| weight | 121.2 | 28.6 | 102.4 | 16.1 | -18.8 | 0.0 | |
| health | 4.0 | 0.9 | 3.8 | 0.9 | -0.2 | 0.0 | |
| N | Pct. | N | Pct. | ||||
| party_p | 共产党员 | 50 | 12.0 | 55 | 14.0 | ||
| 民主党派 | 4 | 1.0 | 6 | 1.5 | |||
| 群众 | 338 | 80.9 | 323 | 82.0 | |||
| computer | 都没有 | 22 | 5.3 | 15 | 3.8 | ||
| 有电脑,无网络 | 28 | 6.7 | 12 | 3.0 | |||
| 有电脑和网络 | 367 | 87.8 | 363 | 92.1 | |||
| aftercls | 否 | 278 | 66.5 | 310 | 78.7 | ||
| 是 | 139 | 33.3 | 84 | 21.3 | 
交叉表
交叉表是对两个或以上离散型变量统计计算而产生的表,二维交叉表是两个变量统计分析的结果。datasummary_crosstab()可以生成二维交叉表。
datasummary_crosstab(sex ~ matbias, data = std_sm)| sex | 是 | 否 | All | |
|---|---|---|---|---|
| 男 | N | 261 | 149 | 418 | 
| % row | 62.4 | 35.6 | 100.0 | |
| 女 | N | 233 | 158 | 394 | 
| % row | 59.1 | 40.1 | 100.0 | |
| All | N | 495 | 309 | 815 | 
| % row | 60.7 | 37.9 | 100.0 | 
三维交叉表是三个变量交叉分析得到的分析表。datasummary_crosstab()可以生成三维交叉表。
std_sm %>%
  datasummary_crosstab(computer ~ sex * aftercls ,
                       statistic = 1 ~ 1 + N + Percent("col"),
                       data= .)| computer | 否 | 是 | 否 | 是 | All | |
|---|---|---|---|---|---|---|
| 都没有 | N | 9 | 12 | 9 | 6 | 38 | 
| % col | 3.2 | 8.6 | 2.9 | 7.1 | 4.7 | |
| 有电脑,无网络 | N | 22 | 6 | 6 | 6 | 41 | 
| % col | 7.9 | 4.3 | 1.9 | 7.1 | 5.0 | |
| 有电脑和网络 | N | 247 | 120 | 293 | 70 | 731 | 
| % col | 88.8 | 86.3 | 94.5 | 83.3 | 89.7 | |
| All | N | 278 | 139 | 310 | 84 | 815 | 
| % col | 100.0 | 100.0 | 100.0 | 100.0 | 100.0 | 
连续型变量分析表
相关系数是描述两个连续型变量之间关系强度一个统计值,datasummary_correlation()可以快速生成多个连续变量之间的相关系数。
#datasummary_correlation
std_sm %>%
  select(stcog, tr_chn, tr_mat, tr_eng, height, weight, health) %>%
  datasummary_correlation()| stcog | tr_chn | tr_mat | tr_eng | height | weight | health | |
|---|---|---|---|---|---|---|---|
| stcog | 1 | . | . | . | . | . | . | 
| tr_chn | .27 | 1 | . | . | . | . | . | 
| tr_mat | .40 | .77 | 1 | . | . | . | . | 
| tr_eng | .35 | .75 | .77 | 1 | . | . | . | 
| height | .08 | −.07 | .03 | −.11 | 1 | . | . | 
| weight | .05 | −.05 | −.02 | −.13 | .56 | 1 | . | 
| health | −.01 | −.06 | −.04 | .00 | .14 | .05 | 1 | 
R表格调整与设计技术
表格设计可以分为两种情况,一种是对其它package生成后的表格进行再调整,比如对modelsummary生成的表格进行调整,另一种情况是对原始数据进行表格的设计。
表格的二次调整
flextable提供了对表格表头、表尾、行、列和单元格调整的各种function。可对文字、背景、边框等多个要素调整,以下是对modelsummary生成表格进一步调整的结果。
library(flextable)
std_sm %>%
  select(tr_chn, tr_mat, tr_eng, stcog, height, weight, health,sex) %>%
  datasummary_balance(~ sex,data = .,output = "flextable",
                      dinm_statistic = "p.value",fmt = 2) %>%
  set_header_labels(
    values =list(`男 (N=418) / Mean`="均值",
                 `男 (N=418) / Std. Dev.`="方差", 
                `女 (N=394) / Mean`="均值",
                `女 (N=394) / Std. Dev.`="方差",
                `Diff. in Means`="均值差",
                p="P值")) %>%
  add_header_row(
    values = c("","男 (N=418)","女 (N=394)",""),
    colwidths = c(1,2,2,2)) %>%
  bold(j=7,bold = TRUE) %>%
  color(i=4,color = "red") %>%
  bg(i=7,bg="lightblue") %>%
  width(j=7,width = 0.5) %>%
  hline(i=3:4,border = fp_border_default()) %>%
  flextable::footnote(j=7,value = as_paragraph("男女均值之间的差值"),
           ref_symbols = c("1"),part = "header") %>%
  set_caption(caption = "Table 1. 数学偏见人群之间的差异对比表")| 男 (N=418) | 女 (N=394) | |||||
|---|---|---|---|---|---|---|
| 
 | 均值 | 方差 | 均值 | 方差 | 均值差 | P值1 | 
| tr_chn | 103.22 | 19.36 | 110.57 | 15.33 | 7.35 | 0.00 | 
| tr_mat | 108.72 | 33.77 | 113.82 | 25.95 | 5.10 | 0.02 | 
| tr_eng | 99.93 | 33.45 | 115.37 | 24.13 | 15.44 | 0.00 | 
| stcog | 10.79 | 3.82 | 10.68 | 3.56 | -0.11 | 0.67 | 
| height | 171.86 | 6.83 | 162.10 | 5.11 | -9.75 | 0.00 | 
| weight | 121.18 | 28.61 | 102.39 | 16.06 | -18.79 | 0.00 | 
| health | 4.04 | 0.92 | 3.80 | 0.88 | -0.24 | 0.00 | 
| 1男女均值之间的差值 | ||||||
kableExtra也提供了对表格表头、表尾、行、列和单元格调整的各种function,也可对文字、背景、边框等多个要素调整,以下是使用kableExtra对modelsummary生成表格进一步调整的结果。
library(kableExtra)
std_sm %>%
  select(tr_chn, tr_mat, tr_eng, stcog, 
         height, weight, health, matbias) %>%
  datasummary_balance(~ matbias,data = .,output = "kableExtra",
                      dinm_statistic = "p.value",
                      title = "数学偏见人群之间的差异对比表",
                    col.names=c("","均值","方差","均值","方差","均值差","P值")) %>%
  kable_styling() %>%
  group_rows("学习成绩", 1,3) %>%
  group_rows("认知能力",4,4) %>%
  group_rows("身体健康",5,7)%>%
  column_spec(6:7, bold = T) %>%
  row_spec(4, bold = T, color = "white", 
           background = "#D7261E",hline_after = FALSE)| 均值 | 方差 | 均值 | 方差 | 均值差 | P值 | |
|---|---|---|---|---|---|---|
| 学习成绩 | ||||||
| tr_chn | 107.2 | 17.4 | 106.6 | 18.5 | -0.6 | 0.6 | 
| tr_mat | 110.4 | 30.2 | 112.9 | 30.3 | 2.5 | 0.3 | 
| tr_eng | 107.3 | 30.3 | 108.7 | 29.7 | 1.3 | 0.6 | 
| 认知能力 | ||||||
| stcog | 10.5 | 3.7 | 11.0 | 3.7 | 0.5 | 0.0 | 
| 身体健康 | ||||||
| height | 167.4 | 7.9 | 166.4 | 7.4 | -1.1 | 0.1 | 
| weight | 113.5 | 25.6 | 110.0 | 24.3 | -3.6 | 0.1 | 
| health | 4.0 | 0.9 | 3.8 | 1.0 | -0.2 | 0.0 | 
对原始表格的直接调整
对原始表格进行美化,两个package组合,一个是formattable和kableExtra,以下是使用这两个package调整后的结果。这两个package的使用方法可参见其官方网站。
#筛选class id为100班级的学生数据
cls_perf <- std_sm %>%
  filter(clsids==100) %>%
  select(ids, stcog, tr_chn, tr_mat,
         tr_eng,stcog, ses_p, height,weight, health, aftercls, sex)
#kableExtra & formattable
library(formattable)
library(kableExtra)
cls_perf %>% 
  arrange(stcog) %>%
  drop_na() %>% 
  mutate(
    stcog=color_tile("white","orange")(stcog),
    health = cell_spec(health, "html", angle = (1:5)*60,
                    background = "red", color = "white", align = "center"),
    aftercls = ifelse(aftercls == "是",
                  cell_spec(aftercls, "html", color = "red", bold = T),
                  cell_spec(aftercls, "html", color = "green", italic = T)),
    tr_eng = cell_spec(tr_eng,color = if_else(tr_eng>120,"#AE0B2A","black")),
    height = color_bar("lightgreen",na.rm=TRUE)(height),
    tr_mat = color_bar("lightblue",na.rm=TRUE)(tr_mat)
  ) %>%
  kable(escape = FALSE,
        col.names = c("ID","认知能力","语文","数学","英语",
                      "经济条件","身高","体重","健康水平","课外班","性别"),
        caption = "Table 1. 3900班学生成绩表") %>%
  kable_styling(bootstrap_options = c("condensed"),
                full_width = FALSE) %>%
  column_spec(7,width = "2cm") %>%
  row_spec(0,color="white",background = "#AE0B2A") %>%
  kableExtra::footnote(general = "数据来源于CEPS.")| ID | 认知能力 | 语文 | 数学 | 英语 | 经济条件 | 身高 | 体重 | 健康水平 | 课外班 | 性别 | 
|---|---|---|---|---|---|---|---|---|---|---|
| 3983 | 4 | 108 | 122 | 107 | 比较富裕 | 164 | 116 | 4 | 否 | 女 | 
| 3984 | 6 | 105 | 126 | 97 | 中等 | 177 | 214 | 2 | 是 | 男 | 
| 3995 | 7 | 105 | 120 | 115 | 中等 | 160 | 88 | 3 | 否 | 女 | 
| 3988 | 9 | 106 | 108 | 109 | 中等 | 160 | 98 | 4 | 是 | 女 | 
| 3990 | 10 | 106 | 138 | 80 | 中等 | 165 | 90 | 3 | 否 | 女 | 
| 3997 | 10 | 99 | 135 | 126 | 中等 | 166 | 110 | 4 | 否 | 男 | 
| 3998 | 10 | 91 | 126 | 100 | 中等 | 169 | 90 | 3 | 是 | 男 | 
| 3991 | 11 | 109 | 125 | 117 | 比较富裕 | 169 | 92 | 3 | 否 | 女 | 
| 3992 | 13 | 103 | 125 | 102 | 中等 | 160 | 120 | 4 | 否 | 女 | 
| 3993 | 13 | 100 | 130 | 107 | 比较富裕 | 173 | 120 | 4 | 否 | 女 | 
| 3985 | 14 | 98 | 133 | 89 | 中等 | 168 | 120 | 4 | 否 | 男 | 
| 3986 | 15 | 105 | 139 | 130 | 中等 | 161 | 100 | 4 | 是 | 男 | 
| 3999 | 16 | 108 | 131 | 121 | 比较困难 | 180 | 148 | 4 | 是 | 男 | 
| Note: | ||||||||||
| 数据来源于CEPS. | 
gt和gtExtras也提供强大的表格设计功能,以下是使用这两个package调整后的结果。这两个package的使用方法可参见其官方网站。
library(gt)
library(gtExtras)
cls_perf2 <- cls_perf %>% 
  mutate(sex=if_else(sex=="男","mars","mercury"),
        aftercls=if_else(aftercls=="是","check","xmark")) %>%
  arrange(stcog) %>% 
  drop_na()
cls_perf2 %>%
  gt() %>%
  gt_hulk_col_numeric(starts_with("tr"),trim=TRUE) %>%
  gt_color_box(stcog, domain = 4:16,
               palette = c("purple",  "green")) %>%
  gt_highlight_cols(ses_p,fill="green",alpha = 0.1) %>%
  gt_fa_column(sex,palette = c("purple","grey")) %>%
  gt_fa_column(aftercls,palette = c("grey","green")) %>%
  gt_fa_rating(health,color="green",icon = "heart") %>%
  gt_duplicate_column(weight) %>%
  gt_plt_bar_pct(weight_dupe) %>%
  cols_move(weight_dupe,weight) %>%
  gt_plt_point(height,
               palette=c("purple", "lightgrey", "green"),width = 15) %>%
  gt_plt_dot(height,ids) %>%
  cols_label(ids="ID",stcog="认知能力",tr_chn="语文",tr_mat="数学", tr_eng="英语",
             ses_p="经济条件",height="身高",health="健康水平",aftercls="课外班",
             sex="性别",weight="体重", weight_dupe="体重指示") %>%
  cols_align("center") %>%
  tab_header(title = "Table 1. N3900班学生基本信息及成绩表") %>%
  tab_source_note("Note: 数据来源于CEPS.") %>%
  tab_style(cell_text(weight = "bold"),locations = cells_column_labels()) | Table 1. N3900班学生基本信息及成绩表 | |||||||||||
| ID | 认知能力 | 语文 | 数学 | 英语 | 经济条件 | 身高 | 体重 | 体重指示 | 健康水平 | 课外班 | 性别 | 
|---|---|---|---|---|---|---|---|---|---|---|---|
| 
    3983
    
    
   | 4 | 108 | 122 | 107 | 比较富裕 | 116 | |||||
| 
    3984
    
    
   | 6 | 105 | 126 | 97 | 中等 | 214 | |||||
| 
    3995
    
    
   | 7 | 105 | 120 | 115 | 中等 | 88 | |||||
| 
    3988
    
    
   | 9 | 106 | 108 | 109 | 中等 | 98 | |||||
| 
    3990
    
    
   | 10 | 106 | 138 | 80 | 中等 | 90 | |||||
| 
    3997
    
    
   | 10 | 99 | 135 | 126 | 中等 | 110 | |||||
| 
    3998
    
    
   | 10 | 91 | 126 | 100 | 中等 | 90 | |||||
| 
    3991
    
    
   | 11 | 109 | 125 | 117 | 比较富裕 | 92 | |||||
| 
    3992
    
    
   | 13 | 103 | 125 | 102 | 中等 | 120 | |||||
| 
    3993
    
    
   | 13 | 100 | 130 | 107 | 比较富裕 | 120 | |||||
| 
    3985
    
    
   | 14 | 98 | 133 | 89 | 中等 | 120 | |||||
| 
    3986
    
    
   | 15 | 105 | 139 | 130 | 中等 | 100 | |||||
| 
    3999
    
    
   | 16 | 108 | 131 | 121 | 比较困难 | 148 | |||||
| Note: 数据来源于CEPS. | |||||||||||