ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

R语言tab1()函数:一行代码生成专业频数分布表与百分比

R语言tab1()函数:一行代码生成专业频数分布表与百分比 做数据分析时最常干的一件事是什么我的答案是看分布。拿到一份数据无论是临床问卷、用户画像还是订单明细我第一件事就是把所有分类变量过一遍频数表。R语言基础包里有个table()函数能列频数但输出就一行行数字百分比、累计百分比都得自己再去算一旦变量多、缺失值也多这个动作就变得很琐碎。后来做流行病学相关的项目接触到了epiDisplay包里面一个tab1()函数让我彻底改掉了只靠table()的习惯——一条命令就把一维频数表、百分比、累计百分比全打出来还顺带出个条形图数据探查阶段的效率直接翻倍。这篇文章就把tab1()的用法、参数、坑以及它和table()的差异完整梳理一遍。文章适合刚学 R 语言、正在做数据清洗或准备写数据说明文档的人参考。1. tab1()到底做了什么table()做不到的事情1.1 一维频数表是什么一维频数表说白了就是统计单个变量里每个取值出现了多少次。比如性别变量有“男”“女”两个取值数一遍得到男 120 人、女 88 人这就是一张一维频数表。它是所有统计分析的起点回归建模之前要看变量分布数据清洗之后要做质量核对写报告时要描述样本基本情况全都要靠它。R 语言里能生成频数表的函数不少最常见的当然是table()。但它解决的问题非常基础只告诉你“每个值出现了多少次”。而一份能直接放进报告、甚至放进论文里的频数表通常还需要百分比、累计百分比、缺失值数量、合计行这些信息。这些内容用table()也能算但要多写好几行代码而且随着变量数量增加重复劳动非常明显。1.2 table()的输出短板我用table()的时候最不舒服的有三点。第一没有百分比。频数是绝对值当两个分组样本量差异很大时光看频数根本没法比较。比如 A 组 500 人中有 300 人选择“是”B 组 80 人中有 60 人选择“是”频数上 B 组的 60 看起来不如 A 组的 300 起眼但转化率完全不同。没有百分比这种信息就漏掉了。第二没有累计百分比。累计百分比对理解变量分布的集中程度非常有帮助。比如某个变量的前两个类别已经占了 80% 以上的样本说明后续类别都是“长尾”在建模时可能要合并。table()不提供这个数值每次都手动cumsum()很麻烦。第三缺失值不直观。table()默认不显示 NA除非你用useNA ifany这类参数。很多人刚学 R 时根本不知道这个参数导致数据里有缺失值却没发现后面分析结果怎么都不对。1.3 tab1()的定位tab1()是epiDisplay包里的函数。这个包最早是为流行病学数据分析设计的所以它的输出风格天然带着“医学论文报表”的味道直接打印频数、百分比、累计百分比同时把缺失值单独列出来还默认画一张条形图。tab1()的适用场景很明确快速、完整、少写代码地查看一个分类变量的分布。它不是用来取代table()的而是在table()之上做了一层“报表格式化”。你给它一个向量它负责把你能想到的描述统计信息全部列出来。# 基础对比 table(mtcars$cyl) # 4 6 8 # 11 7 14 tab1(mtcars$cyl, graph FALSE) # Frequency Percent Cum. percent # 4 11 34.38 34.38 # 6 7 21.88 56.26 # 8 14 43.75 100.00 # Total 32 100.00 100.00看到区别了吗tab1()输出的已经是一张可以直接用的报表了。这也是我为什么说它适合“快速数据探查”和“数据说明文档”两个场景。2. 环境准备与最小可用示例2.1 安装epiDisplay包tab1()不在 R 基础包里需要先安装epiDisplay。安装方式和其他包一样install.packages(epiDisplay) library(epiDisplay)如果你在公司内网或者镜像源比较慢可以指定镜像install.packages(epiDisplay, repos https://cloud.r-project.org)这个包有一定的历史依赖的包不算少第一次安装可能会花一点时间。如果安装时提示缺少某个依赖包直接按提示安装对应包即可。装好之后加载时屏幕上可能滚动出不少信息包括“masked”之类的提示这个到第 5 节再仔细说先不用管。2.2 第一个tab1()调用拿 R 自带的mtcars数据集来演示。这个数据集记录了 32 辆汽车的型号、油耗、气缸数、变速箱类型等信息做教学示例特别合适。先看气缸数cyl的分布tab1(mtcars$cyl, graph FALSE)屏幕输出大概是这样的mtcars$cyl : Frequency Percent Cum. percent 4 11 34.38 34.38 6 7 21.88 56.26 8 14 43.75 100.00 Total 32 100.00 100.00只写了一行代码频数、百分比、累计百分比、合计行全都有了。第一次用的时候我确实觉得“这才叫报表”而不是一串没格式的数字。2.3 读懂输出tab1()输出其实很好读但我还是见到不少新手对着它发呆。这里把每一列的含义说清楚输出列含义计算方式Frequency该取值出现的次数直接计数Percent该取值占样本总量的百分比Frequency / Total * 100Cum. percent累计百分比当前行及之前所有 Percent 累加比如cyl有 4 个气缸的车是 11 辆占总样本 32 辆的 34.38%4 缸和 6 缸加起来占 56.26%这就是累计百分比的含义。注意tab1()的累计百分比是按表格当前行的顺序累加的如果你修改了因子水平的顺序或者后续用了sort.group TRUE累计百分比会相应变化。理解这一点很重要下面会专门讲到。另外tab1()执行后会返回一个table对象你可以把它赋值给一个变量留着后续用tbl - tab1(mtcars$cyl, graph FALSE) class(tbl) # table不过要注意赋值之后它仍然会在控制台打印输出。如果你希望“静默执行、只保留返回值”可以把输出包装进invisible()或者用capture.output()忽略它tbl - capture.output(tab1(mtcars$cyl, graph FALSE))这个方法在后面对多个变量一起跑的时候很实用可以避免控制台被刷屏。3. 把tab1()用出花样的参数细节tab1()看着简单但有几个参数在实际项目中非常好用。我按使用频率逐个说。3.1 sort.group按频数降序显示默认情况下tab1()按照变量取值的“自然顺序”排列。如果变量是因子就按因子水平顺序如果是字符型就按字母顺序。但很多时候我们更希望看到“出现最多的类别排在最前面”这样能一眼看出分布集中在哪里。tab1(mtcars$cyl, sort.group TRUE, graph FALSE)输出会变成Frequency Percent Cum. percent 8 14 43.75 43.75 4 11 34.38 78.13 6 7 21.88 100.00 Total 32 100.00 100.00频数最高的 8 缸车排在了第一行累计百分比也随之重新计算。这个参数在做市场调研数据、问卷多选题整理时特别有用——你关心的永远是“哪个选项被选得最多”。但这里有个坑如果变量本身是有序因子比如“低”“中”“高”用sort.group TRUE会把业务顺序打乱反而不好读。要不要排序取决于你要呈现什么信息。我的建议是无序分类变量排序有序分类变量保持原顺序。3.2 控制百分比的精度和累计列有些场景下你不需要累计百分比比如只打算展示每个类别的占比。这时可以关掉累计列让表格更简洁tab1(mtcars$cyl, cum.percent FALSE, graph FALSE)另外tab1()允许通过参数控制小数点位数方便统一报表格式。不同 R 版本里这个参数名称可能有细微差异使用前建议先跑一下?tab1看帮助文档。我个人的习惯是用于自己快速探查时保留累计百分比输出给合作方或写报告时关闭累计列、统一小数位这样表格看起来更干净。3.3 graph参数图形输出与后续ggplot2的衔接tab1()默认graph TRUE也就是说如果你不设置graph FALSE它除了打印表格还会画一张条形图。这个功能在 RStudio 里很方便一步到位看到分布形态。但如果你是在写 R Markdown 报告或者批量跑多个变量建议把graph FALSE关掉。原因有两个批量循环时每个变量弹一张图会在文档里堆出一长串条形图排版很难看。tab1()的绘图用的是基础绘图系统样式偏老和ggplot2的审美差距很大。所以我的建议是探查阶段关掉图形先用表格看数字确定要展示的变量后再用ggplot2精心画图。这样两全其美。3.4 na.omit缺失值处理一维频数表一个很容易被忽略的功能是对缺失值的展示。当向量里有 NA 时tab1()默认会把它作为一行单独列出来x - c(是, 否, 是, NA, 否, 是) tab1(x, graph FALSE)输出中会出现一个NA的行并展示缺失值占比这对数据质量检查非常友好。你一眼就能看出“这个变量有 16.67% 的数据是缺失的”。如果确认缺失值不影响当前分析想直接从输出中忽略它可以用na.omit TRUEtab1(x, na.omit TRUE, graph FALSE)关键提醒na.omit TRUE只是让缺失值不显示在输出里并不会修改原始数据向量。如果你后续还要做别的分析缺失值仍然在原数据里别误以为它被清理掉了。3.5 批量循环多变量一键生成频数表实际项目中几乎不可能只看一个变量。建模前我经常要一口气看十几个分类变量的分布。tab1()放在循环里非常顺滑vars - c(cyl, am, gear, carb) for (v in vars) { cat(\n, v, \n) tab1(mtcars[[v]], graph FALSE) }这里我用mtcars[[v]]而不是mtcars$v是因为$后面的名称不会被变量v的值替换这是一个很隐蔽的初学者坑。循环里统一用双中括号取列是最稳妥的做法。批量跑完每个变量的频数表、百分比、缺失值情况全部呈现在控制台数据底细一下午就能摸清。4. 数值变量与连续数据不能直接套用时的处理思路4.1 连续变量直接丢进tab1()之后tab1()虽然好用但它本质上处理的是“离散的取值”。如果你把一个连续变量直接丢进去比如mtcars$mpg每加仑行驶英里数会发生什么它会把每个不同的数值都当成一个类别去统计。mtcars只有 32 行mpg 的取值却有 25 个不同的数值最后生成的频数表会有 25 行每行频数大多是 1 或 2阅读价值很低。所以拿到一个变量先问自己它是分类变量、有序分类变量还是连续变量如果答案是连续变量那就不能直接套用tab1()需要先分组。4.2 用cut()分组后再做频数表连续变量分组最常用的函数是cut()。它可以把数值范围切成几段生成一个有序因子。比如我想把mpg分成五组mtcars$mpg_grp - cut(mtcars$mpg, breaks c(10, 15, 20, 25, 30, 35), right FALSE, include.lowest TRUE) tab1(mtcars$mpg_grp, graph FALSE)breaks指定了分组的边界值right FALSE表示区间是“左闭右开”的也就是说[10, 15)包含 10 但不包含 15include.lowest TRUE保证最小的 10 也被包含进第一个区间。分组之后连续变量就变成了一个有序分类变量tab1()的频数、百分比、累计百分比就都有了统计意义。特别是累计百分比这时候能很直观地告诉你“油耗低于 20 的车占了多少比例”很适合做样本分布描述。4.3 因子水平顺序对输出行序的影响tab1()输出行的顺序和因子水平的顺序完全一致。如果变量是字符型R 默认按字母顺序排但业务场景往往需要特定顺序比如“小学、初中、高中、大学、研究生”。正确做法是在创建因子时显式指定levelsedu - factor(c(大学, 小学, 研究生, 初中, 高中), levels c(小学, 初中, 高中, 大学, 研究生)) tab1(edu, graph FALSE)这样输出的顺序就是业务顺序而不是拼音或字母顺序。这个细节看起来不起眼但我在交付数据说明文档时被合作方纠正过好几次从那以后凡是涉及有顺序的分类变量我一定会在因子阶段就把顺序定好。反过来如果因子含有多余的水平但实际数据中没有任何样本tab1()仍然会把这些空水平显示出来频数为 0。这在数据清理时会造成干扰。解决办法是先做droplevels()再传给tab1()或者用之前提到的na.omit思路手动删掉空行。5. 实战排查加载epiDisplay时的函数冲突与调用规范5.1 函数被屏蔽的问题第一次加载library(epiDisplay)的时候控制台大概率会弹出类似这样的提示The following object(s) are masked from package:xxx: ...这是 R 里常见的“函数名冲突”epiDisplay包里的某些函数和其他包里的函数重名后加载的包会把先加载的包里的同名函数“屏蔽”掉。如果你同时加载了dplyr、tidyr这类包冲突可能更明显。遇到这种提示先别慌大多数情况下不影响tab1()的使用。但如果后续某个函数突然报错“could not find function”或者行为和你预期不一致就要怀疑是不是命名空间冲突了。5.2 用epiDisplay::tab1()完整限定方式最稳妥的解决办法是不依赖加载顺序直接用“包名::函数名”的方式调用。epiDisplay::tab1(mtcars$cyl, graph FALSE)这种写法的好处是不管当前 R 会话里加载了多少个包都能明确告诉 R“我要用的是 epiDisplay 里的这个函数”不会因为屏蔽关系产生歧义。我现在的项目脚本里基本都采用这个写法尤其是在写较长分析脚本时可读性和稳定性都更好。如果你确实不想看到冲突提示也有一些办法。比如先加载epiDisplay再加载其他包让epiDisplay的函数被后面的包屏蔽而不是反过来或者用detach()在分析结束后卸载它detach(package:epiDisplay, unload TRUE)但说实话这些方法都只能缓解问题最干净的还是全限定调用。5.3 常见报错与规避我在使用tab1()过程中遇到过几类报错这里一并整理出来省得大家再踩一遍。报错一could not find function tab1原因很简单包没加载或者加载失败。解决办法是library(epiDisplay)之后再执行如果还是不行检查安装过程有没有报错。用epiDisplay::tab1()完整写法可以直接绕过这个问题。报错二传入数据框时报错tab1()期望的输入是一维向量如果你直接传一个数据框进去它会报错。正确做法是用$或[[取出一列再传入# 错误示例 tab1(mtcars, graph FALSE) # 正确示例 tab1(mtcars$cyl, graph FALSE) tab1(mtcars[[cyl]], graph FALSE)报错三中文因子水平显示乱码这通常不是tab1()的问题而是 R 基础绘图系统或控制台的编码问题。如果你只是输出表格一般不会乱码如果开了graph TRUE画条形图中文标签在部分系统上会显示成方块。建议是图形部分交给ggplot2tab1()只负责生成表格。报错四空因子的干扰上一节提到的空水平问题tab1()会把频数为 0 的空水平也打印出来造成表格看起来很长、很乱。用droplevels()清理后再传入即可。tab1(droplevels(factor_var), graph FALSE)6. 使用心得什么场景值得用什么场景别用6.1 推荐场景用了这么久我总结出tab1()最值得用的三个场景。第一个是数据清洗刚开始时的“摸底”。拿到一份不知道底细的数据先对性别、年龄分组、地区、婚姻状况、学历这些变量快速跑一遍tab1()缺失值、异常取值、某个类别占比过高基本一眼就能发现。第二个是写数据说明文档。被叫去写“样本基本情况表”的时候tab1()的输出格式已经非常接近论文里的三线表了我甚至直接把输出贴到 Word 里再稍作整理比自己在 Excel 里数半天靠谱。第三个是课堂或培训讲统计基础。tab1()把频数、百分比、累计百分比同时列出来对初学者理解“分布”这个概念非常直观。我自己带新人时经常先让他跑几个tab1()建立“看数据先看分布”的习惯。6.2 不推荐场景tab1()也不是万能的。进入正式建模阶段之后我基本就不再依赖它了。原因是它的输出是一个基础table对象不是整洁数据框tidy data。如果你想后续接dplyr管道、或者用ggplot2画高度定制的图还得先把table转成数据框再重命名列反而多了一道工序。这时候我一般直接用library(dplyr) mtcars %% count(cyl) %% mutate(percent n / sum(n) * 100, cum_percent cumsum(percent))dplyr::count()加mutate()虽然代码略长但返回值是标准的整齐数据框后续想怎么处理都行。如果你更喜欢极致简洁的 tidy 风格也可以试试janitor::tabyl()它输出的也是整洁格式的频数表配合adorn_totals()函数还能加合计行。另外如果你的数据集超大、变量极多也不要指望tab1()一把梭。它一次只处理一个变量更适合“重点变量逐个看”不适合“全数据集自动扫描”。那种场景用summary()、skimr之类的函数更高效。6.3 一个值得记住的输出转换技巧最后分享一个很实用的小技巧。尽管tab1()的输出是打印在控制台上的但你可以把返回的table对象转成数据框然后导出成 CSV方便后续在 Excel 或 Word 里二次加工tbl - epiDisplay::tab1(mtcars$cyl, graph FALSE) df_out - as.data.frame(tbl) names(df_out) - c(group, frequency) write.csv(df_out, cyl_freq.csv, row.names FALSE)转出来的数据框列名不一定好看用names()重新命名即可。这样你既享受了tab1()的“一站式输出”又没有把自己绑死在控制台文本上。我现在的工作流里tab1()基本只用在做初步数据探查和写数据说明文档时一旦进入正式建模阶段我会用dplyr::count()搭配ggplot2重新整理。但如果你想快速给出一张能看的频数分布表tab1()仍然是那个代码最少、结果最省事的选项。工具没有高下之分重要的是每个阶段都选最顺手的那个。
返回列表