资讯动态

从tableone到三线表:R语言基线特征表排版与导出指南

发布时间:2026/10/27 15:18:56 来源:尧图企业网站定制
1. 先把三线表和 tableone 的关系理清楚R 语言里做基线特征表绕不开tableone这个包。它做的事情很集中把一组变量按分组变量拆开连续变量算均值标准差或中位数四分位数分类变量算频数百分比自动挑一个合适的假设检验最后吐出一个可以直接贴进论文的矩阵。而三线表是国内期刊和学位论文里最主流的表格样式——顶线、栏目线、底线三条横线没有竖线两边不封口。这两件事本身是独立的tableone负责算三线表负责排版。很多人卡住不是因为不会算而是因为tableone默认打印出来的东西是一坨带引号的字符矩阵直接复制到 Word 里线全是乱的。我见过不少同行最后手动在 Word 里画表格、一个一个数字敲进去240 例的样本量敲一下午。这篇内容就是把算和排版这两段接起来把中间那些参数、坑、导出路径全部摊开讲。先明确适合谁看。如果你刚接触 R只会read.csv和summary这篇文章能让你在一个下午内产出一张能直接交稿的三线表如果你已经用了一段时间tableone但每次导出都要手工返工那第 3 节的几种导出方案和第 4 节的排查表应该能省掉你不少重复劳动。文中所有数据都是我按常见临床数据结构模拟的变量名、分组名都是虚构的你照着换成自己的字段就行。提示tableone的输出对象不是 data.frame而是一个 S3 类的 list。很多人第一次用as.data.frame(tab)发现结果不对就是因为跳过了print()这一步。后文会专门讲这个中间态。1.1 三线表的三条线到底各管什么很多人以为三线表就是少画几条线其实每条线的语义是固定的弄错了审稿人一眼就能看出来。顶线top rule压住整个表头区域它的粗细通常等于或略粗于底线中间的栏目线midrule分隔表头和数据体一般比顶线细一半底线bottom rule收住数据体和顶线同粗。三条线之外表格内部一律不加横线更不能加竖线。粗细的具体数值没有全球统一标准但有几套常见实践可以参考。国内学位论文和多数中文期刊的习惯是顶线底线 1.5 磅、栏目线 0.75 磅英文期刊投出去的话booktabs宏包默认是\toprule0.08 em、\midrule0.05 em、\bottomrule0.08 em换算到 10 pt 字号大概就是 1.1 磅和 0.7 磅。我在实际项目里一般先用 1.5 / 0.75 这一套等期刊给了模板再改因为改线宽比改整个表格结构便宜太多。还有一个容易被忽略的点三线表的表注放在底线下方而不是表格内部。表注通常要写清楚缩写含义比如 BMI、SMD、IQR、检验方法说明连续变量正态分布用 t 检验非正态用秩和检验、以及缺失值处理方式。这些内容tableone的explain TRUE参数会自动生成一段英文说明但直接拿去用不合适我一般关掉它自己写。1.2 tableone 替我干掉了哪些手工活不用tableone的话一张标准基线表你要做这些事对每个连续变量判断正态性Shapiro-Wilk 或者看偏度正态的算 mean±sd 走 t 检验非正态的算 median(IQR) 走 Wilcoxon对每个分类变量算频数百分比走卡方期望频数小于 5 还得换 Fisher多分组还要考虑方差分析和 Kruskal-Wallis算完把结果拼成一个矩阵再处理小数点位数、百分号、P 值格式。二十个变量就是二十次重复劳动而且每次数据更新都要重来一遍。tableone把这些全部封装进CreateTableOne()一个函数调用。它的核心价值不在于能算而在于规则统一同一张表里所有连续变量用同样的小数位所有 P 值用同样的格式所有分类变量用同样的n (%)写法。这一点在团队协作里特别重要我待过的一个项目组三个人各自出的表小数点位数都不一样合并的时候光对齐格式就花了两天。它还有两个附加能力值得单独提。一个是smd TRUE直接输出标准化均差做倾向性评分匹配或者加权的时候SMD 比 P 值更能反映组间平衡性因为 P 值受样本量影响太大——大样本下 0.1 的均值差也能算出显著小样本下明显的不平衡反而 P 0.05。另一个是addOverall TRUE加一列总体描述很多期刊要求第一列是全体样本手工加这一列很容易算错分母。1.3 环境准备与数据准备里那几个必踩的坑装包本身没什么难度install.packages(tableone)一行搞定但如果要出三线表还得把排版链上的包一起装上install.packages(c(tableone, knitr, kableExtra)) # 如果要导出 Word install.packages(c(flextable, officer)) # 如果要导出 gt 风格 install.packages(gt)kableExtra在 Linux 或者某些较新的 R 版本上编译时会依赖systemfonts和textshaping如果报错说找不到harfbuzz那是系统库缺失得先装系统级的开发包。这一条在 Windows 上基本不会遇到但如果你在服务器上跑 RStudio Server八成会撞上。数据准备这块我踩过最多次的坑是变量类型。tableone判断一个变量是连续还是分类逻辑是这样的先看factorVars参数里有没有显式指定没有的话看数据本身是不是 factor还不是的话就当成连续变量处理。所以如果你从 CSV 读进来的性别列是1和2它会被当成连续变量算均值和标准差输出一个 1.48 ± 0.50 这种毫无意义的结果。解决办法很简单读数据之后统一做一次类型规整dat$sex - factor(dat$sex, levels c(男, 女)) dat$stage - factor(dat$stage, levels c(I, II, III)) dat$group - factor(dat$group, levels c(对照组, 试验组))levels的顺序不是可有可无的。tableone输出分类变量时按 levels 顺序排列如果不指定R 默认按字母或拼音排序对照组可能排在试验组后面表格读起来别扭。更隐蔽的问题是分组变量本身如果group是字符型strata传进去之后列的顺序也是按字母排的你以为是对照组 vs 试验组实际输出是反的。还有一类数据问题更麻烦数值型变量里混进了0.5、未测这种文本。read.csv默认会把这些列读成 character然后整列被当成分类变量一个本该连续的指标变成几十个水平的因子表瞬间炸开。我现在的习惯是读数据时加na.strings c(, NA, 未测, -)把常见的缺失标记一次性转成 NA。2. CreateTableOne 的参数体系拆解CreateTableOne()的参数看起来不多但每一个都影响最终输出的形态。我按使用频率从高到低拆一遍重点讲清楚为什么这么设而不是罗列文档。2.1 vars、strata、factorVars 三个核心参数library(tableone) tab - CreateTableOne( vars c(age, sex, bmi, stage, sbp), strata group, data dat, factorVars c(sex, stage), addOverall TRUE )vars是要进表的变量名向量顺序就是你希望的表格行顺序这一点很实用——不用事后调整行序直接在vars里排好就行。我一般按人口学 → 疾病特征 → 实验室指标 → 结局的逻辑排读表的人顺着看下来不费劲。strata是分组变量只能填一个。这里有个限制要知道tableone不支持同时按两个变量分层。如果你要做分期 × 治疗组的交叉表得自己拆成子集分别出表或者用后文第 5 节提到的封装函数。factorVars是保险栓。凡是分类变量哪怕数据里已经是 factor 了我也建议在这里再写一遍。原因是有时候数据经过merge或者subset操作factor 的属性会掉成 charactertableone拿到 character 列会当成连续变量处理。显式指定一遍成本几乎为零但能避免一类很难发现的错误。2.2 正态与非正态nonnormal 参数怎么定CreateTableOne()本身不判断正态性它默认所有连续变量都是正态分布输出 mean ± sd 并用 t 检验或方差分析。要改成中位数四分位数得在print()阶段传nonnormal参数nonnormal_vars - c(bmi, sbp) print(tab, nonnormal nonnormal_vars, showAllLevels TRUE, pDigits 3, contDigits 2, catDigits 1)注意这是print()的参数不是CreateTableOne()的。这个设计初看别扭实际有它的道理同一个表对象可以用不同的nonnormal设置打印多次方便你对比。那到底哪些变量该进nonnormal我的实操流程是这样先对每个连续变量按组做 Shapiro-Wilk样本量大于 5000 的时候 Shapiro 过于敏感改用偏度和峰度偏度绝对值小于 1 视为近似正态同时看直方图如果明显右偏比如住院天数、某些炎症指标、费用类数据即使 Shapiro 没拒绝也归到非正态里去。医学数据里真正需要警惕的是那些天然右偏的指标硬套 t 检验会被审稿人挑。有个折中做法值得一试对同一个变量分别按正态和非正态各出一版看结论有没有实质差异。如果 P 值一个 0.03 一个 0.04那说明结论稳健选哪个都行如果一个 0.04 一个 0.12那就要谨慎了这种情况下我倾向于用非正态的版本因为它对极端值更不敏感保守一点不容易出错。注意nonnormal里的变量名必须是vars里出现过的写错了不会报错只会静默失效——你以为用了秩和检验实际还是 t 检验。建议把nonnormal_vars单独定义成变量出表后核对一下 test 那一列。2.3 检验方法自动选择的逻辑tableone选检验方法的规则是可以推演的理解了这套规则你就能预判输出、也能在结果反常时快速定位。连续变量两组 正态 →t.test()默认 Welch不假设方差齐多组 正态 →aov()单因素方差分析两组 非正态 →wilcox.test()多组 非正态 →kruskal.test()。分类变量默认chisq.test()不做连续性校正。如果某个格的期望频数小于 5tableone会给出警告并建议用 Fisher但不会自动切换——你得自己在print()里传exact c(变量名)。print(tab, exact c(stage), nonnormal c(bmi, sbp))这里有个细节exact只对 2×2 表完全有效。如果分组是三组以上分类变量又是三水平fisher.test()在多分类情况下计算量爆炸tableone会直接拒绝并报错。遇到这种情况我的处理办法是把罕见水平合并比如把 III 期和 IV 期合并成晚期或者改用chisq.test(simulate.p.value TRUE)的蒙特卡洛近似——不过tableone不直接支持后者得手动算。另一个可以强制指定的参数是testNormal、testNonNormal、testApprox、testExact它们的值可以是default、TRUE、FALSE或者具体函数名。比如你想对所有连续变量统一用 Welch t 检验而不让tableone自己选可以传testNormal t.test。这个用法比较小众但在写方法学部分的时候有用——你可以在论文里明确写所有连续变量均采用 Welch 两样本 t 检验而不是含糊地说采用合适的检验方法。2.4 SMD 和 P 值该看哪个smd TRUE打开后表格会多出一列 SMD。连续变量的 SMD 计算是$$d \frac{\bar{x}_1 - \bar{x}_2}{\sqrt{(s_1^2 s_2^2)/2}}$$二分类变量用的是另一套$$d \frac{p_1 - p_2}{\sqrt{[p_1(1-p_1) p_2(1-p_2)]/2}}$$多分类变量的 SMD 计算更复杂一些tableone内部按 Austin 提出的多分类扩展方法处理输出的是一个综合值。这些公式你不需要手算但知道分母用的是合并标准差而不是对照组标准差能帮你理解为什么 SMD 和 Cohens d 的数值会略有差异。判断标准上流行病学领域普遍接受 0.1 作为平衡的阈值0.1 到 0.2 之间算轻度不平衡超过 0.2 就值得关注。但我要提醒一句SMD 是描述性的没有 P 值也不该拿来卡 0.05 的线。它的价值在于跨研究可比——同样的两组人P 值会随样本量变SMD 不会。做匹配前后对比的时候把匹配前后的 SMD 并排放比放两列 P 值有说服力得多。ExtractSmd(tab)可以把 SMD 单独抽成一个向量做图或者做平衡性诊断表的时候很方便smd_vec - ExtractSmd(tab) smd_vec[smd_vec 0.1]3. 从 tableone 对象到三线表的完整实现这一节是全文的核心。前面算出来的tab只是一个 list要变成能贴进文档的三线表中间要过一道print()或者summary()。我把三条路径都走一遍你可以按自己的输出目标选。3.1 print 的返回值和矩阵中间态先理解这个中间态。print(tab, printToggle FALSE)返回的是一个 character matrix行是变量分类变量展开后每个水平一行列是分组、P 值和检验方法mat - print(tab, nonnormal c(bmi, sbp), showAllLevels TRUE, printToggle FALSE, quote FALSE, noSpaces TRUE) class(mat) # [1] matrix array dim(mat)quote FALSE去掉引号noSpaces TRUE去掉变量名里的空格默认会把age变成age带个尾随空格导出去之后很难看。这两个参数几乎每次都要加我干脆写进了自己的模板函数。矩阵的第一列是行名mat本身没有把行名放进列里。转 data.frame 的时候要注意df - as.data.frame(mat, stringsAsFactors FALSE) df - cbind(变量 rownames(df), df) rownames(df) - NULL此时df的列大概是变量、Overall、对照组、试验组、p、test。列名会带上原数据里的分组名如果分组水平的命名不规范比如group0、group1这里也是原样输出所以前面强调过 levels 的命名要认真定。还有一个参数值得单独说showAllLevels。设为TRUE时三水平的分类变量会展开成三行第一行是变量名加上总体分布设为FALSE时只显示一行通常显示的是最高水平或者cramVars里指定的压缩变量。二分类变量如果设showAllLevels FALSE只显示比例较高的那个水平表格能省一半行数。期刊对这一点要求不一中文期刊多数要求展开英文期刊接受压缩。我一般先按展开做等编辑部提意见了再压。3.2 方案一kableone 加 kableExtra 走 booktabstableone自带一个kableone()函数本质是把print()的结果丢给knitr::kable()再叠加booktabs TRUElibrary(knitr) library(kableExtra) tab_mat - print(tab, nonnormal c(bmi, sbp), showAllLevels TRUE, printToggle FALSE, quote FALSE, noSpaces TRUE) kableone(tab, booktabs TRUE, longtable TRUE) %% kable_classic(full_width FALSE, font_size 10) %% row_spec(0, bold TRUE) %% footnote( general_title 注, general 连续变量正态分布以均数±标准差表示非正态以中位数四分位数间距表示分类变量以频数百分比表示。SMD 为标准化均差。, footnote_as_chunk TRUE )booktabs TRUE就是让它输出\toprule、\midrule、\bottomrule在 LaTeX 环境里编译出来就是标准三线表。kable_classic是kableExtra的样式函数full_width FALSE让表格不撑满整个页面宽度这个是三线表的默认审美——线不封口表格就不该占满版心。这条路径适合输出 HTML 或者 PDFLaTeX。输出 HTML 的时候booktabs的效果是通过 CSS 的 border 实现的kable_classic会把上下边框设成 2px、表头下边框设成 1px看起来就是三线表的比例。如果要更精细地控制线宽可以用kable_styling()配合自定义 CSS但多数场景没必要。有一个坑要提前说kableone()的参数传递有版本差异。旧版本的kableone()不接受nonnormal参数它内部调print()时用的是默认设置所以非正态变量在 kable 输出里还是 mean±sd。绕过办法是先手动print()出矩阵再kable()kable(tab_mat, booktabs TRUE, align c(l, c, c, c, c, c)) %% kable_classic(full_width FALSE)这个写法我在多个项目里用过稳定性最好因为完全绕开了kableone()的封装。3.3 方案二转 data.frame 后交给 flextable 出 Word如果要交 Word 稿flextable是最省事的选择它的theme_booktabs()直接就是三线表样式library(flextable) library(officer) df - as.data.frame(tab_mat, stringsAsFactors FALSE) df - cbind(变量 rownames(df), df) rownames(df) - NULL ft - flextable(df) %% theme_booktabs() %% font(fontname Times New Roman, part all) %% fontsize(size 9, part all) %% bold(part header) %% align(align center, part all) %% align(j 1, align left, part all) %% valign(valign center, part all) %% set_table_properties(layout autofit) %% fix_border_issues() doc - read_docx() %% body_add_par(表1 各组基线特征比较, style centered) %% body_add_flextable(ft) %% body_add_par(注连续变量正态分布以均数±标准差表示……, style Normal) print(doc, target table1.docx)theme_booktabs()做的事情在 Word 里体现为表格顶部和底部各一条粗线表头下方一条细线其余边框全部去掉。这是最接近期刊要求的效果。fontsize(size 9)对应小五号正文一般用五号10.5表格小一号是通行做法。align(j 1, align left)把第一列变量名左对齐数值列居中——这一点很重要第一列如果居中长短不一的变量名看起来会很散。fix_border_issues()是一个很多人不知道的函数。flextable在合并单元格或者应用主题之后边框有时会出现该有线的地方没线、该没线的地方有线的情况这个函数是用来重置边框计算的。我遇到过导出的 Word 表格底线只画了一半加了这一行就好了。autofit()和set_table_properties(layout autofit)有区别。前者按内容估算列宽后者控制 Word 表格的自动调整属性。中文环境下我一般两个都加否则 Word 打开后列宽可能被压得很窄中文变量名换行换得很难看。3.4 方案三gt 包做出版级排版如果目标读者更看重美观或者你要把表放进网页报告gt是另一个选择library(gt) df %% gt() %% tab_header(title md(**表1 各组基线特征比较**)) %% cols_align(align center, columns -变量) %% cols_align(align left, columns 变量) %% tab_style( style cell_borders(sides c(top, bottom), weight px(2), color black), locations cells_column_labels() ) %% tab_style( style cell_borders(sides bottom, weight px(2), color black), locations cells_body(rows nrow(df)) ) %% tab_options( table.border.top.style hidden, table.border.bottom.style hidden, column_labels.border.bottom.width px(1), table_body.hlines.style hidden, table.font.size px(12) ) %% tab_source_note(注连续变量正态分布以均数±标准差表示……)gt的优点是精细到每一个单元格的边框都能单独控制缺点是代码长而且导出 Word 需要gtsave()走 HTML 中转格式偶尔会飘。我的实际用法是内部汇报和网页用gt交稿用flextableLaTeX 投稿用kableExtra。3.5 表注、缩写和缩进这些收尾细节表格主体做完剩下的都是收尾工作但恰恰是这些细节决定了表格专业不专业。表注我一般分三层写。第一层说明数据表达方式连续变量怎么写、分类变量怎么写第二层说明统计方法正态用什么检验、非正态用什么检验、分类变量用什么检验Fisher 用在哪些变量上第三层说明缩写BMI、SBP、SMD、IQR 各是什么。三层之间用分号隔开整体放在底线下方字号比表体小半号。分类变量展开成多行之后变量名在第一行下面几行是各水平。这时候需要把水平的名字缩进两格否则读者分不清哪行是变量名、哪行是水平。flextable里的做法是给这些行加一个前缀is_level_row - !grepl(^[a-zA-Z\u4e00-\u9fa5] \\(|^[a-zA-Z]$, df$变量) df$变量[is_level_row] - paste0( , df$变量[is_level_row])更稳妥的做法是在print()阶段就记录下哪些行是水平行。tableone输出的行名有一个规律变量名行通常带单位或者变量名本身水平行是纯水平名。我的经验是直接根据原始vars列表去匹配比正则可靠。还有一个细节P 值列如果有0.001这样的字符串整列会被当成 character排序和对齐都会出问题。flextable里无所谓但如果后面要做条件着色比如 P 0.05 标红就得先处理。我的处理方式是加一列数值型 P 值专门用来判断展示列保持字符。4. 常见问题与排查实录这一节记录的是我自己和别人项目里真实撞过的问题按出现频率排。很多问题在文档里找不到答案因为它们是环境和数据形态耦合出来的。4.1 变量类型识别错误的连锁反应症状某个明明是分类的变量在表里显示成45.2 ± 12.7这种数字。原因前面提过是factorVars没指定或者数据本身是数值编码。但还有更隐蔽的变体变量在数据里是 factorlevels 却是0和1输出成0 (45.0)这种读者根本不知道 0 代表什么。解决办法是在出表前统一重命名 levelsdat$hypertension - factor(dat$hypertension, levels c(0, 1), labels c(无, 有))还有一种情况数值型变量里混了几个NAtableone默认includeNA FALSENA 会被直接剔除分母变小。如果缺失比例高比如超过 10%必须在表注里说明缺失例数否则数据完整性会被质疑。要显示缺失情况可以把includeNA TRUE传给CreateTableOne()它会多出一行NA但这一行的呈现方式不太好看我一般选择在表注里手动写一句某变量缺失 n 例。4.2 P 值变成 NA 或者检验直接报错P 值出现 NA 通常有三种原因。第一种是某组内该变量全是同一个值方差为 0t 检验算不出来第二种是分类变量某个水平在某组里一例都没有卡方统计量无法计算第三种是样本量太小自由度不足。排查顺序建议这样先看tableone的警告信息它会明确告诉你是哪个变量出的问题然后用table(dat$group, dat$stage)手工看列联表确认有没有空格连续变量用tapply(dat$bmi, dat$group, var)看各组方差。如果确认是空格导致的最直接的办法是把罕见水平合并。如果合并之后还是不行就老实标 NA 并在表注里写因某组样本量不足未进行统计学检验。硬凑一个 P 值出来比标 NA 危险得多。还有一种报错值得单独提Error in fisher.test(): FEXACT error 7。这是 Fisher 精确检验在 2×2 表但样本量或者频数太大的时候出现的fisher.test()的工作内存不够。解决方法是改用fisher.test(..., simulate.p.value TRUE, B 1e5)但tableone不支持传这个参数只能手动算完再填进去。4.3 中文变量名和字体导致的导出乱码这个问题在 Windows 上尤其常见。表现是flextable导出的 Word 里中文变成方框或者问号。原因通常是字体设置里指定的字体不包含中文字形比如只设了Times New Roman而没处理中文。我的处理方式是分开设置西文用 Times New Roman中文用宋体。flextable支持通过fontname传一个命名的字符向量但更稳的做法是用officer的样式控制或者干脆全程用宋体——宋体本身包含西文字形虽然不如 Times 好看但不会出错。ft - flextable(df) %% theme_booktabs() %% font(fontname 宋体, part all) %% fontsize(size 9, part all)如果走 PDF LaTeX 路线中文需要ctex宏包或者xelatex引擎kableExtra输出表格时要在 YAML 里指定latex_engine: xelatex并加载booktabs和ctex。这一步配错了会报! Package inputenc Error: Unicode character 不 (U4E0D) not set up看到这个错基本就是引擎没配对。4.4 三线表的线宽在 Word 里不生效theme_booktabs()默认的线宽在 Word 里显示得比较细有些期刊要求顶线底线达到 1.5 磅。flextable允许单独覆盖ft - ft %% border_remove() %% hline_top(border fp_border(width 1.5, color black), part header) %% hline_bottom(border fp_border(width 0.75, color black), part header) %% hline_bottom(border fp_border(width 1.5, color black), part body)注意顺序先border_remove()清掉所有边框再逐条加。如果先加再 remove加的线会被清掉。part header指表头区域part body指数据体hline_bottom(part body)画的就是整个表格的底线。还有一个坑如果表格里有合并单元格比如表头跨列合并hline_*系列函数作用在合并单元格上时线可能只画在第一个格子上。这时候fix_border_issues()能救回来一部分但更彻底的办法是先画线再合并或者干脆避免在表头做合并。4.5 常见问题速查表现象最可能的原因处理方式分类变量显示为均值±标准差未指定 factorVars 或数据为数值编码显式传 factorVars或将列转为 factorP 值为 NA组内方差为 0或列联表存在空格合并罕见水平或标注未检验非正态变量仍是 mean±sdnonnormal 参数写在 CreateTableOne 里了nonnormal 是 print() 的参数kableone 输出丢失 nonnormal 设置版本差异封装未透传改用手动 print kable导出 Word 中文乱码字体不含中文字形fontname 设为宋体或思源黑体底线只画了一半合并单元格后边框计算错乱加 fix_border_issues()Fisher 检验报 FEXACT error 72×2 表频数过大改蒙特卡洛近似或手工计算后填入分组列顺序反了分组变量 levels 未指定factor(levels c(对照组,试验组))变量名带尾随空格print 时 noSpaces 默认为 FALSE加 noSpaces TRUE表格行数比预期多很多showAllLevels 默认 TRUE设 FALSE 或指定 cramVars5. 把出表流程封装成可复用的函数单次出表用上面这些代码足够了但如果一个项目里有十几个表基线表、亚组表、敏感性分析表每次都复制粘贴两百行代码不现实。我现在的做法是写一个薄封装把算 排版 导出三步串起来。5.1 一个够用的三线表函数make_table1 - function(data, vars, strata, factor_vars, nonnormal_vars NULL, exact_vars NULL, caption 基线特征, out_file NULL) { tab - CreateTableOne( vars vars, strata strata, data data, factorVars factor_vars, addOverall TRUE, smd TRUE ) mat - print( tab, nonnormal nonnormal_vars, exact exact_vars, showAllLevels TRUE, printToggle FALSE, quote FALSE, noSpaces TRUE, pDigits 3, contDigits 2, catDigits 1, smd TRUE, missing TRUE ) df - as.data.frame(mat, stringsAsFactors FALSE) df - cbind(变量 rownames(df), df) rownames(df) - NULL ft - flextable(df) %% theme_booktabs() %% font(fontname 宋体, part all) %% fontsize(size 9, part all) %% bold(part header) %% align(align center, part all) %% align(j 1, align left, part all) %% set_table_properties(layout autofit) %% fix_border_issues() if (!is.null(out_file)) { doc - read_docx() %% body_add_par(caption, style centered) %% body_add_flextable(ft) %% body_add_par(注连续变量正态分布以均数±标准差表示非正态以中位数四分位数间距表示分类变量以频数百分比表示。, style Normal) print(doc, target out_file) } invisible(list(table tab, matrix mat, flex ft)) }这个函数的几个设计取舍值得说一下。返回值里我塞了三样东西原始tab对象方便后面ExtractSmd或者重新打印、中间矩阵方便自己再加工、flextable对象方便追加样式。返回invisible是为了不让它在控制台刷屏。missing TRUE这个参数我在很多项目里都开着它会在表里加一行缺失值计数。虽然多数期刊不要求但审稿人问起来的时候有据可查。5.2 多分组和分层表怎么处理tableone不支持一次按两个变量分层但实际项目里按分期分层再看组间差异是常见需求。我的处理方式是循环加合并stage_levels - levels(dat$stage) res_list - lapply(stage_levels, function(s) { sub - subset(dat, stage s) tab - CreateTableOne( vars c(age, sex, bmi), strata group, data sub, factorVars sex ) mat - print(tab, printToggle FALSE, quote FALSE, noSpaces TRUE) df - as.data.frame(mat) df - cbind(分层 s, 变量 rownames(df), df) rownames(df) - NULL df }) final - do.call(rbind, res_list)这样出来的是一张长表每行带分层标签。如果想做成宽表每个分期一组列得用reshape或者手工拼列麻烦一些但可控。我在实际项目里更倾向长表因为宽表列数一多就排不下三线表横着摊开十几列在 A4 纸上根本放不下。多分组三组以上的情形tableone原生支持strata传进去就行它会自动用方差分析或者 Kruskal-Wallis。要注意的是三组以上的分类变量检验卡方会给出一个总体 P 值但看不出哪两组之间有差异。这种情况我会在表注里加一句组间两两比较采用 Bonferroni 校正然后单独做一篇事后检验的结果附在补充材料里。5.3 tableone 的边界在哪里用了几年下来我对tableone的定位是基线表专用工具它能覆盖 80% 的常规需求剩下 20% 需要换工具。它不擅长的地方包括复杂的多层表头比如对照组下面再分治疗前/治疗后两列这个flextable的merge_at()能解决但代码量陡增加权数据的基线表虽然tableone支持svy设计对象但参数的传递很容易出错以及需要大量条件格式的场景比如按 P 值着色、按 SMD 加粗。替代方案里gtsummary的tbl_summary()是功能最接近也最现代的选择它的语法更接近 tidyverse 风格输出直接支持gt、flextable、kable多种后端而且对多层表头和合并单元格的支持比tableone好。我在新项目里已经开始往gtsummary迁移理由是它的add_p()、add_difference()、add_overall()是链式调用的参数比print()那一堆更符合直觉。但tableone有一个优势是gtsummary目前比不了的ExtractSmd()和summary()系列函数给出的结构化输出在做匹配平衡诊断的时候更顺手。我的建议是如果项目只需要一张标准基线表tableone够用且学习成本低如果项目要出十几张不同类型的表或者要做多层表头直接上gtsummary会更省心。两者的底层统计逻辑是一致的切换成本主要在语法习惯上。最后分享一个我自己踩过的坑。有次交稿前发现表里的 P 值全是三位小数但期刊要求两位。我以为pDigits 2改一下就行结果改完发现有个变量显示成0.00——它实际是 0.004四舍五入之后变成 0.00 了比显示0.001还糟。后来我的做法是统一用三位小数同时在表注里写P 值保留三位小数小于 0.001 记为 0.001这样既不会丢精度也不会出现 0.00 这种尴尬值。这种细节文档里不会写只有真交过稿的人才会在意。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑