
做临床数据、流行病学调查或者任何带分组比较的课题最后真正卡住人的往往不是回归模型而是那张放在论文第一页的Table 1。审稿人扫一眼你的基线表基本就能判断这批数据是怎么来的、分组是不是随机、统计描述有没有偷懒。我用 R 语言里的 tableone 包画三线表已经有很多次了从最开始把CreateTableOne()的输出直接截图贴进 Word到后来能把一张表做到期刊排版级别的干净中间踩的坑不算少。这篇就聊聊 tableone 绘制三线表这件事它是什么、能帮你把哪一段重复劳动省掉、哪些参数是真正决定成败的、以及从原始数据框到一张能直接投稿的三线表中间到底要经过几道手。不管你是刚接触 R 的研究生还是已经用惯了 SPSS 想换条路走的老手下面这些内容都应该能直接拿去用——代码是可运行的参数是带解释的坑是我自己踩过的。1. 先想清楚为什么基线表值得单独抠一遍1.1 三线表在论文里的位置和它的隐性规范三线表这个东西本质上是一种极简主义的排版约定整张表只有三条横线——顶线、栏目线、底线没有竖线没有多余的装饰线靠留白和对齐来区分层次。它之所以在学术出版里站住脚是因为读者的眼睛在扫描数据时不需要跟网格线打架。但真正麻烦的地方在于这条极简的规则背后还压着一大堆没人明说、却会被编辑挑出来的隐性规范顶线和底线通常要粗一些1.5 磅左右栏目线要细0.75 磅表题放在表格上方并且编号表注放在表格下方用来说明缩写和检验方法连续变量的单位必须塞进表头而不是数值里P 值不能出现 0.000 这种写法而要写成 0.001。这些细节单拎出来都不难难的是它们要在一张几十行的表里同时成立。而基线表恰恰是所有表格里最长的那一类十几个变量、每个分类变量还要展开成多个水平、再加上总体列和分组列一张表很容易拉到一整页。手工在 Word 里调这种表改一个数字就可能把整行的对齐搞乱改一次分组就得从头再来一遍。所以这类表格的正确做法从来不是画而是生成——让代码负责数值和结构人只负责最后的排版微调。我个人的判断标准很朴素如果一张表你需要在投稿前改三遍以上换分组、加变量、调小数位、补缺失值说明那它就绝对不该手工做。基线表几乎百分之百落在这一档里。1.2 为什么我最后选了 tableone 而不是别的方案R 里能做描述性统计表的工具其实不少我大致把它们分成三类各自的取舍差别挺大。方案典型工具优势代价纯手工汇总dplyr tidyr 自定义函数完全可控任何奇怪需求都能实现每个变量类型都要写一套逻辑代码量翻倍容易出错专用表格包tableone、gtsummary、compareGroups一行函数出全表内置检验和 SMD默认输出需要二次加工样式定制受包的限制报告框架Rmarkdown kableExtra / flextable排版精细可直接出 Word/PDF学习曲线陡且有额外的依赖管理成本tableone 的位置很讨巧它足够轻只依赖很少的几个包输出的是一个规规矩矩的矩阵/数据框你拿到手之后想怎么改都行它又足够懂行知道基线表需要什么——分组列、总体列、每种变量类型对应的检验方法、标准化均数差SMD、缺失值计数这些东西它默认就替你算了。相比之下gtsummary 的默认输出更漂亮但它是围绕 gt 对象构建的你想把某一行单独拿出来改个写法得先搞清楚 gt 的对象结构而 tableone 给你的是一个 matrixmat[3, 2] - ...就能改非常粗暴但非常有效。还有一点是我比较看重的tableone 的检验逻辑是显式的。你指定了nonnormal它就用秩和检验你没指定它就用 t 检验你指定了exact它就用 Fisher 精确检验。这种你说了算的设计写方法学部分的时候特别好交代不会出现软件自己选了某个检验而我解释不清的尴尬。提示tableone 适合结构由代码生成、样式由人收尾的工作流。如果你追求一键出终稿它可能不是最优解如果你追求的是数值绝对可靠、结构随时可改它是性价比最高的那一档。2. CreateTableOne 的核心参数逐个拆解2.1 数据进入函数前的三个前置动作很多人第一次用 tableone 出问题根子都不在函数上而在数据本身。我在跑CreateTableOne()之前固定会做三件事这三件事花不了两分钟但能省掉后面半小时的排查。第一件是确认变量类型。R 读入 Excel 或 CSV 的时候经常把本该是分类的变量读成字符型或者数值型把本该是数值的变量因为混进了未知未查这类文本而整列变成字符。我的习惯是先跑一遍str(df)或者sapply(df, class)把结果跟自己的变量清单对一遍。数值型的看class是不是numeric或integer分类的看是不是factor。第二件是给分类变量显式指定水平顺序。这一步特别容易被忽略但影响很大factor(x, levels c(Never,Former,Current))和factor(x)出来的表格行序是不一样的后者按字母排前者按你给的逻辑排。基线表的行序应该是有临床意义的——比如疾病分期从轻到重、吸烟状态从从不到当前而不是按拼音或字母表排。第三件是提前想清楚哪些变量要进表。我的原则是基线表里只放分组之前就已经确定的变量。年龄、性别、基线 BMI、既往病史、分期这些都可以放治疗后的疗效评价、随访期间出现的事件一律不能放。这条原则看着简单但我在审稿意见里见过不止一次因为把术后并发症塞进基线表被质疑的案例。# 三个前置检查建议固定写成一小段脚本 str(demo) sapply(demo, class) sapply(demo, function(x) sum(is.na(x))) # 每个变量的缺失数2.2 vars、strata、factorVars 三件套怎么配CreateTableOne()的核心参数其实只有三个vars决定哪些变量进表strata决定按什么分组factorVars告诉函数哪些变量要当分类变量处理。前两个很直观第三个是新手最容易漏的。为什么factorVars这么关键因为 tableone 判断一个变量该走连续变量那套逻辑还是分类变量那套逻辑主要看它的类型。如果一列是numeric但你其实想把它当分类比如分期被编码成 1/2/3/4函数会默认给你算均数和标准差出来的结果完全不是你要的。反过来如果一列是character函数会当分类处理但水平的排列顺序就不受你控制了。所以我现在的写法是先明确列出连续变量和分类变量两个向量再分别传给函数这样代码本身就是一份文档别人接手也能看懂。library(tableone) # 连续变量默认按正态处理 cont_vars - c(age, bmi, sbp, dbp) # 分类变量 cat_vars - c(sex, smoke, stage, histology, grade) all_vars - c(cont_vars, cat_vars) tab - CreateTableOne( vars all_vars, data demo, strata group, factorVars cat_vars, addOverall TRUE # 加一列总体 )addOverall TRUE这个参数我基本每次都会开。原因很实际审稿人经常要求先描述总体再描述各组而且有些期刊的格式就是总体 / A 组 / B 组三列并排。与其后面手工拼一列不如一开始就让函数算好。2.3 正态性判断与 nonnormal 参数的实际取舍nonnormal是 tableone 里最有争议的一个参数因为哪些变量算非正态这件事本身就没有标准答案。函数默认所有连续变量都是正态的走均数 ± 标准差加 t 检验你在nonnormal里列出来的那些改走中位数四分位数间距加秩和检验。我的实际做法是分层判断而不是一刀切样本量够大每组 30 以上、分布肉眼看着对称的直接按正态处理不去纠结 Shapiro-Wilk 的 P 值。样本量一大正态性检验几乎必然拒绝原假设这时候看 P 值没有意义。明显右偏的指标比如住院天数、费用、某些肿瘤标志物、等待时间一律放进nonnormal。这类变量的均值没有临床解释力报中位数更诚实。有明确行业惯例的跟着惯例走。比如血压、身高体重、年龄绝大多数期刊接受均数 ± 标准差而生存时间、住院时长几乎都要求中位数和四分位数。这条判断最好在跑表之前就定下来并且写进分析计划里。我见过不少人在看到 P 值不显著之后回头改nonnormal列表这在方法学上是站不住的——检验方法必须在看结果之前确定。nonnormal_vars - c(los, cost, crp) tab - CreateTableOne( vars all_vars, data demo, strata group, factorVars cat_vars, addOverall TRUE ) print(tab, nonnormal nonnormal_vars, showAllLevels TRUE, smd TRUE, printToggle FALSE)2.4 检验方法、exact 与 smd 的开关逻辑默认情况下tableone 对连续变量用 t 检验、对分类变量用卡方检验。当某个分类变量的理论频数太小通常是有格子期望频数小于 5卡方就不靠谱了这时候要把这个变量放进exact参数里改用 Fisher 精确检验。这里有个经验不要一看到期望频数小于 5就全表改 Fisher。我的做法是先跑一遍默认的看summary(tab)里哪些变量的检验结果旁边有警告或者 P 值明显异常再针对性地把那一两个变量放进exact。全表都上 Fisher 虽然保守但会让方法学部分变得很难写——你得解释为什么所有变量都用精确检验。另外minimize参数值得提一句。当某个分类变量水平数超过 2 时Fisher 精确检验的计算量会指数级上升minimize TRUE会让函数在计算 P 值时采用更省时的算法结果基本一致但速度快很多。表格里分类变量超过 3 个水平的时候我一般会开这个。smd TRUE是另一个我几乎必开的开关。标准化均数差在观察性研究里比 P 值更有意义因为它不受样本量影响能真实反映组间差异的大小。现在的趋势是随机对照试验报 P 值可以观察性研究的基线表最好同时给出 SMD一般以 0.1 作为平衡良好的经验阈值。注意SMD 对连续变量和分类变量都能算但分类变量的 SMD 是基于各水平比例算的解读时不要跟连续变量的 SMD 直接横向比较。3. 从原始数据到三线表的完整实操3.1 造一份能跑通的模拟数据为了让下面的每一步都能复现我先造一份结构接近真实研究的数据。320 例、两组、包含连续变量和分类变量、故意留一些缺失值——这样后面讲缺失值处理的时候才有东西可讲。set.seed(20240501) n - 320 demo - data.frame( id sprintf(P%03d, 1:n), group factor(sample(c(Control, Treated), n, TRUE, prob c(0.5, 0.5)), levels c(Control, Treated)), age round(rnorm(n, 58, 11)), sex factor(sample(c(Male, Female), n, TRUE, prob c(0.56, 0.44)), levels c(Male, Female)), bmi round(rnorm(n, 24.3, 3.6), 1), smoke factor(sample(c(Never, Former, Current), n, TRUE, prob c(0.55, 0.22, 0.23)), levels c(Never, Former, Current)), stage factor(sample(c(II, III, IV), n, TRUE, prob c(0.35, 0.42, 0.23)), levels c(II, III, IV)), los rpois(n, 9), # 住院天数右偏 cost round(rlnorm(n, 9.6, 0.5)), # 费用强右偏 egfr round(rnorm(n, 82, 18), 1) ) # 人为制造一些缺失 set.seed(7) demo$bmi[sample(n, 12)] - NA demo$egfr[sample(n, 6)] - NA这份数据里los和cost是典型的右偏变量应该进nonnormalage、bmi、egfr按正态处理sex、smoke、stage是分类变量。这个设定跟真实课题里的判断几乎一模一样。3.2 一步步生成 Table 1先做变量分组再生成表格。cont_vars - c(age, bmi, egfr, los, cost) cat_vars - c(sex, smoke, stage) all_vars - c(cont_vars, cat_vars) tab - CreateTableOne( vars all_vars, data demo, strata group, factorVars cat_vars, addOverall TRUE ) # 先看默认输出确认结构没问题 print(tab)第一次跑出来的结果我一般会重点看三处一是行数对不对分类变量应该按水平展开二是缺失值有没有被单独统计出来三是 P 值那一列有没有莫名其妙的 NA。这三处没问题再往下做格式。接下来是正式的打印参数配置。这一段是整个流程里最影响成品质量的环节。tab_mat - print( tab, nonnormal c(los, cost), exact c(stage), # 分期里 IV 期样本偏少用精确检验 showAllLevels TRUE, # 分类变量显示全部水平 smd TRUE, catDigits 1, # 分类变量百分比保留 1 位 contDigits 1, # 连续变量保留 1 位 pDigits 3, # P 值保留 3 位 quote FALSE, noSpaces TRUE, printToggle FALSE ) class(tab_mat) dim(tab_mat)printToggle FALSE这个参数是关键它让print()不去控制台打印而是把结果作为一个矩阵返回赋值给变量之后你就能随意处置。quote FALSE和noSpaces TRUE是为了去掉引号和多余空格让后面导出时干净。拿到这个矩阵之后第一件事是看看它长什么样head(tab_mat, 8)输出的矩阵大概长这样列名会随分组名变化OverallControlTreatedptestn320160160age (mean (SD))58.2 (10.9)58.5 (11.2)57.9 (10.6)0.612sex Female (%)141 (44.1)68 (42.5)73 (45.6)0.631stage (%)0.041stage II (%)112 (35.0)66 (41.3)46 (28.8)这里有个细节值得展开连续变量的行名自带(mean (SD))或(median [IQR])后缀分类变量的行名是变量名 水平 (%)的形式。这些行名在导出成三线表的时候通常是要改的——中文期刊的表头一般写成年龄岁x̄±s或者年龄[岁MP25P75]。所以拿到矩阵之后我一般会先处理行名。# 把行名改成中文标签 rn - rownames(tab_mat) rn[rn age (mean (SD))] - 年龄岁x̄±s rn[rn bmi (mean (SD))] - BMIkg/m²x̄±s rn[rn egfr (mean (SD))] - eGFRmL/minx̄±s rn[rn los (median [IQR])] - 住院天数dMP25P75 rn[rn cost (median [IQR])] - 住院费用元MP25P75 rn[rn sex Female (%)] - 女性 rn[rn sex Male (%)] - 男性 rn[rn stage II (%)] - II 期 rn[rn stage III (%)] - III 期 rn[rn stage IV (%)] - IV 期 rn[rn stage (%)] - 分期 rownames(tab_mat) - rn这里我用了两个空格做缩进是为了在三线表里体现水平从属于变量的层次感。有些期刊不允许缩进那就改成在变量名后面加冒号或者直接把水平行写成性别女性这种形式。3.3 print() 的输出参数与格式微调除了上面用到的还有几个参数值得单独说一下它们的默认值经常不是你想要的那个。showAllLevels默认是TRUE但很多人会手动改成FALSE。改成FALSE之后分类变量只显示第一个水平作为参照后面跟个 P 值。这种做法在英文期刊里很常见因为表格能压缩掉将近一半的行数。但如果你的分类变量有 3 个以上水平只显示第一个水平会让读者看不出其他组的分布我不推荐。catDigits和contDigits分别控制百分比和连续变量的小数位。我的默认是catDigits 1百分比保留一位小数、contDigits 1连续变量保留一位。有些指标需要两位比如 eGFR、某些生化指标那就单独设contDigits 2。原则是同一张表里同一个指标的小数位必须一致不要出现58.2 (10.9)和24.35 (3.62)混排。pDigits默认是 3但我建议另外加一步处理把小于 0.001 的 P 值统一改成0.001。tableone 本身不做这个转换它可能给你输出0.000。这一步必须手工做因为0.000在学术出版里是明确的错误写法。# 把 p 列里的 0.000 改成 0.001 p_col - which(colnames(tab_mat) p) p_num - suppressWarnings(as.numeric(tab_mat[, p_col])) idx - !is.na(p_num) p_num 0.001 tab_mat[idx, p_col] - 0.001还有一个容易被忽略的点test那一列在最终表格里其实是不要的。它记录的是每个变量用了什么检验方法信息很有用但通常写在表注里而不是表格里。我的做法是先把这一列单独存下来导出前从矩阵里删掉。test_info - tab_mat[, test] tab_mat - tab_mat[, colnames(tab_mat) ! test, drop FALSE]3.4 导出成三线表CSV、Word、HTML 三条路矩阵准备好之后导出方式取决于你最终要交什么。路线一CSV 中转 Excel。这是最土但最稳的办法。write.csv()出去用 Excel 打开选中区域加边框把中间的竖线和内部横线删掉只留顶线、栏目线、底线然后调粗细。适合完全不懂 LaTeX、也不需要自动化的人。write.csv(tab_mat, table1_raw.csv, fileEncoding UTF-8)注意Excel 打开 UTF-8 的 CSV 在部分 Windows 环境下会乱码稳妥的做法是用fileEncoding GB18030或者在 Excel 里用数据 - 从文本/CSV导入并手动指定编码。路线二knitr::kable 直接出三线表。如果你用 Rmarkdown 写论文或者做补充材料这条路最省事。knitr::kable( tab_mat, booktabs TRUE, # 启用三线表样式 caption 表1 两组患者基线资料比较, align c(l, c, c, c, c), linesep )booktabs TRUE会让 kable 在 LaTeX 输出里使用\toprule、\midrule、\bottomrule这三个命令出来的就是标准三线表。HTML 输出下它也会做相应的样式处理但视觉效果跟 LaTeX 有差距。路线三flextable 出 Word。如果目标期刊要求交 Word这条路最可控因为 flextable 生成的flextable对象可以直接写进 docx而且能精确控制边框粗细。library(flextable) ft - as.data.frame(tab_mat) ft - cbind( rownames(tab_mat), ft) rownames(ft) - NULL ft - flextable(ft) ft - border_remove(ft) # 先清掉所有边框 ft - hline_top(ft, part header, border fp_border(width 1.5)) ft - hline_bottom(ft, part header, border fp_border(width 0.75)) ft - hline_bottom(ft, part body, border fp_border(width 1.5)) ft - bold(ft, part header) ft - align(ft, align center, part all) ft - align(ft, j 1, align left, part all) # 第一列左对齐 ft - autofit(ft) save_as_docx(ft, path table1.docx)这段代码里最关键的三个函数调用是border_remove()加上三次hline_*。border_remove()先把默认的网格线全部清掉然后hline_top()画粗顶线、hline_bottom(part header)画细栏目线、hline_bottom(part body)画粗底线——三条线一条不多一条不少这就是三线表的物理定义。fp_border(width ...)里的单位是磅1.5 磅对应顶线和底线0.75 磅对应栏目线这个比例是我试过最接近多数期刊要求的。3.5 手工修补三线表的最后 10%代码能做到 90%剩下的 10% 一定得手工。我通常在 Word 里做四件事。第一件是合并变量名单元格。分类变量的第一行是变量名比如分期后面几行是各水平这三行的第一列应该合并成一个单元格并垂直居中。flextable 里有merge_v()可以做但 Word 里手动合并三秒就完了而且能顺便检查有没有合错行。第二件是补表注。表注一般三句话缩写说明BMI体质指数eGFR估算肾小球滤过率、统计描述说明连续变量以 x̄±s 或 MP25P75表示分类变量以 n%表示、检验方法说明组间比较采用 t 检验、Mann-Whitney U 检验或 χ² 检验。这三句话一定要跟表格里实际用的方法对得上我见过表格里写了 Fisher 精确检验但表注写卡方的属于硬伤。第三件是对齐检查。数值列一律右对齐或小数点对齐第一列变量名列左对齐表头居中。这一步纯粹是眼睛的活但做完之后整张表的观感会完全不同。第四件是打一遍数字。随机挑五六个格子回到 R 里用原始数据手算一遍确认均值、中位数、百分比对得上。这步听起来多余但我在项目里真的遇到过因为分组变量编码有误导致整列数字偏移的情况早发现能省掉后面一大堆返工。4. 常见问题与排查技巧实录4.1 变量类型踩坑连续变量被当成因子因子被当成数值这是 tableone 使用中最高频的问题没有之一。症状很典型本该出均数和标准差的地方出了百分比或者本该出百分比的地方出了均值。根因只有两个。第一数据列本身是字符型函数自动当分类处理。这通常是因为原始数据里混进了NA、-、未查这类文本。第二数据列是数值型但你想当分类忘记写进factorVars。排查方法很简单跑一遍sapply(demo, class)把结果跟你的变量清单逐行对照。数值型但应该是分类的补进factorVars字符型但应该是数值的先做清洗再as.numeric()。# 清洗一个混进文本的数值列 demo$bmi_clean - suppressWarnings(as.numeric(demo$bmi_raw)) sum(is.na(demo$bmi_clean)) - sum(is.na(demo$bmi_raw)) # 差值是转换失败的个数提示suppressWarnings()只是把警告藏起来转换失败的会变成NA。所以一定要用上面这个减法核对一下确认失败个数在可接受范围内。如果失败个数很多说明数据里确实混了非数值内容得回去看原始记录。4.2 P 值全是 NA 或缺失P 值列出现 NA一般是三种情况之一。第一种是某个分组下该变量的样本量为 0 或 1。比如某个分期只有 1 例落在对照组卡方算不出来。这种情况要么合并水平把 III 期和 IV 期并成III-IV 期要么把这个变量从表里拿掉要么在表注里注明该变量因样本量过少未做检验。第二种是分组变量本身有缺失。strata指定的那一列如果有NA那些行会被整行排除导致各组样本量对不上你预期的数字。解决方法是在建表前先看一眼table(demo$group, useNA ifany)。第三种是变量是常数。某一列所有值都一样标准差为 0任何检验都无法计算。这种情况通常说明这个变量在你的样本里没有变异直接删掉就好。# 三个快速排查 table(demo$group, useNA ifany) # 分组是否有缺失 sapply(demo[all_vars], function(x) length(unique(x))) # 是否有常数变量 sapply(demo[all_vars], function(x) sum(is.na(x))) # 缺失分布4.3 分层过多、表格太宽怎么压缩一张表如果有 3 个分组、再加总体列就已经 4 列数据了如果每组还要显示例数百分比宽度很容易超出版面。压缩的办法有几个按推荐程度排序。优先考虑的是合并分类变量的水平。分期原来是 II、III、IV 三个水平如果 III 和 IV 的样本量都不大合并成III-IV 期是合理的而且临床上也常常这么分。这一步必须在看结果之前做不能因为 P 值不显著就合并那是数据驱动属于方法学问题。次选是调整为showAllLevels FALSE只显示参照水平。这个做法省空间但丢信息只适合分类变量水平之间没有实质差异的情况。再其次是把部分变量挪到补充材料。主表只放最核心的十个左右变量其他的放附表正文里写一句其余基线资料见附表 S1。这是我现在最常用的策略主表干净审稿人看着也舒服。还有一个技术手段是换更紧凑的表示法连续变量如果不需要同时报均数和标准差可以只报均数分类变量如果不是每个水平都重要可以只报阳性例数/总数。但这类压缩要谨慎别把审稿人需要的信息压没了。4.4 缺失值处理与Unknown行tableone 默认会把缺失值单独列一行通常在变量下面显示Missing但只在有缺失的时候才出现。这个设计很实用因为基线表本来就应该交代缺失情况。但有两个坑。一是默认情况下includeNA FALSE也就是缺失值不计入百分比的分母这时候各水平百分比加起来可能不是 100%。二是如果某个变量的缺失比例很高超过 10%只在表里列一行Missing是不够的正文里必须交代缺失机制和处理方式。我的做法是缺失比例低于 5% 的表里列一行就够了5% 到 10% 之间的在表注里说明超过 10% 的考虑做多重插补或者敏感性分析并且绝对不能简单地用均值填补——那样会人为压低方差让后续所有检验都偏乐观。# 缺失比例一览 miss_rate - sapply(demo[all_vars], function(x) mean(is.na(x))) round(sort(miss_rate, decreasing TRUE), 3)如果确实需要在表里显示缺失可以显式打开print(tab, includeNA TRUE, showAllLevels TRUE, printToggle FALSE)打开之后每个有缺失的分类变量下面会多一行Missing数值是缺失例数和占比。这一行的位置有时会排在所有水平之后有时会排在最前面取决于版本导出前记得核对一下。4.5 常见问题速查表症状最可能的原因处理办法连续变量出现百分比列是字符型被当分类处理清洗后转numeric或确认是否真的该当分类分类变量出现均数±标准差数值编码的分类变量未写进factorVars补进factorVars或先转成factorP 值显示 NA某组该变量样本量过少/为常数/分组有缺失合并水平、删变量或核查分组列百分比加总不等于 100有缺失值分母被排除打开includeNA TRUE或调整分母口径表格行序混乱分类变量未指定levels建表前factor(x, levels c(...))小数位不统一catDigits与contDigits设置不一致统一设置或导出后手工统一Excel 打开 CSV 乱码UTF-8 与本地编码不匹配用GB18030写出或改用导入功能Word 里表格边框改不干净flextable 未先border_remove()先清边框再逐条添加三条线5. 一些让表格更能过审的细节5.1 数字精度、单位与脚注的三条硬规矩精度这件事我的经验是连续变量保留一位小数百分比保留一位小数P 值保留三位小数这套组合能覆盖绝大多数期刊。例外情况是本身就要求高精度的指标比如某些生化指标、基因表达量那就单独设contDigits 2或3。关键是同一张表里同一类指标必须统一不要出现58.2和24.35混排。单位必须进表头不能进数值。写成年龄岁x̄±s而不是年龄 58.2 岁。这不只是排版习惯问题更是因为数值列里混进单位之后任何排序和计算都会失效。脚注要覆盖三件事缩写、统计描述方式、检验方法。这三件事必须跟表格内容严格对应。我通常会在 R 里把实际用到的检验方法导出成一个字符向量比如unique(test_info)然后照着这个写脚注避免表里用秩和检验、脚注写 t 检验这种低级错误。# 导出实际使用的检验方法方便核对脚注 unique(test_info[!is.na(test_info) test_info ! ])5.2 表格与正文的一致性检查清单投稿前我会做一遍固定检查清单不长但每次都能查出问题表格里的分组名与正文、与图注里的分组名是否完全一致不要一处写试验组、一处写治疗组各组样本量之和是否等于总样本量总数是否与流程图里的数字对得上每个变量的缺失例数是否与正文数据完整性段落里描述的一致表里的 P 值格式是否统一有没有漏改的0.000表注里提到的每个缩写是否都在表中出现过反之亦然三线表的顶线、栏目线、底线粗细是否分别是 1.5、0.75、1.5 磅有没有残留的竖线这套检查看起来繁琐但真做下来也就五六分钟而且能拦下绝大多数会被编辑部退回来的格式问题。我个人的体会是基线表这种看起来最没技术含量的东西恰恰是审稿人用来判断你做事是否细致的窗口——一张行序合理、小数位统一、脚注齐全的三线表传递出的信号远比它本身的信息量要大。后面如果要扩展我一般会把 tableone 的建表逻辑封装成一个函数参数只留数据框、分组变量、连续变量列表、分类变量列表、非正态变量列表这五个项目里换一批数据只要改这几行半小时的活能压到五分钟。