Base R直方图原理与实战:从分箱到密度的统计可视化根基
1. 项目概述:用基础R语言画直方图,不是调包,是真正理解数据分布的起点
“How to Make a Histogram in Base R”——这个标题看似简单,但背后藏着统计可视化最根本的一课:不依赖ggplot2、不调用tidyverse生态,只用R自带的graphics包,从零构建一个能真实反映数据结构、可解释、可复现、可教学的直方图。我带过几十期数据分析工作坊,发现一个惊人现象:超过70%的学员能熟练写出ggplot(data, aes(x = var)) + geom_histogram(),却说不清breaks = 30到底在切什么、freq = FALSE和prob = TRUE的区别在哪、为什么同一组数据用不同xlim会彻底改变对偏态的判断。这说明,我们正在批量生产“会敲命令但不会读图”的人。而Base R直方图,恰恰是那把解剖刀——它强制你面对每一个参数背后的统计含义。它不漂亮,但诚实;它不自动,但可控;它不智能,但可教。这篇文章不是教你怎么“快速出图”,而是带你亲手拆开hist()函数的引擎盖,看清柱子怎么立、频数怎么算、密度怎么归一、边界怎么定。适合三类人:刚学R的新手(避开生态迷雾)、需要讲授统计基础的讲师(所有参数都可当板书案例)、以及在生产环境中必须确保图形完全可复现的工程师(无外部依赖,R 3.0+全兼容)。你不需要任何额外安装,只要打开R Console,输入hist(c(1,2,2,3,4,4,4,5)),你就已经站在了数据可视化的原点。
2. 核心设计逻辑与方案选型:为什么坚持用Base R,而不是“更高级”的方案?
2.1 直方图的本质不是“画柱子”,而是“分箱+计数+映射”的三步不可逆操作
很多人误以为直方图只是“把数据按区间分组后画条形图”。这是严重误解。直方图的核心是连续变量的离散化建模,其数学定义是:对随机变量X,在区间[a,b]上定义分箱(bin),每个bin对应一个频数n_i或概率密度f_i = n_i / (N × w_i),其中w_i是第i个bin的宽度,N是总样本量。关键点在于:bin的宽度w_i可以不等长(如对数分箱),但面积才代表概率,而非高度。Base R的hist()函数正是严格遵循这一定义设计的——它默认输出的是频率密度(density)对象,包含mids(中点)、counts(频数)、intensities(密度值)、breaks(断点)等完整字段。而ggplot2的geom_histogram()默认绘制的是频数(count),且y轴标签常被误标为“count”而非“density”,导致大量初学者在做密度估计时直接套用stat = "count",结果得到错误的核密度曲线叠加。我曾帮一家风控团队复现一份监管报告,他们用ggplot2画的直方图在theme_minimal()下y轴自动缩放,导致两个本应面积相等的分布(训练集vs测试集)看起来高度差异巨大,差点引发模型漂移误判。换成Base R后,一行hist(train, freq = FALSE, col = "lightblue"); hist(test, freq = FALSE, col = "salmon", add = TRUE),叠加清晰、面积可比、无需调整坐标轴——因为Base R不“美化”,它只“计算”。
2.2hist()函数的四大核心参数:breaks、freq、xlim、main,每一个都直指统计本质
Base R直方图的“简陋”恰恰是它的力量来源。它只暴露四个最核心的控制参数,逼你直面选择:
breaks:不是“柱子数量”,而是分箱策略的声明。它可以是数字(如breaks = 20,表示尝试生成约20个等宽bin)、向量(如breaks = c(0,10,20,50,100),明确定义断点)、或字符串方法名(如breaks = "Sturges"、“Scott”、“Freedman-Diaconis”)。这里没有“智能推荐”,只有你对数据分布的认知。Sturges公式适用于近似正态数据(bin数 ≈ 1 + log₂(N)),而Freedman-Diaconis用四分位距IQR替代标准差,对异常值鲁棒性极强。我处理电商订单金额时,用breaks = "Sturges"得到30多个窄柱子,全是噪声;改用breaks = "FD"后,柱子锐减到9个,清晰呈现“<100元高频区”、“100–500元次高频区”、“>500元长尾区”三大结构。freq:这是最容易被忽略的“开关”,却决定y轴的物理意义。freq = TRUE(默认)时,y轴是频数(count),柱子高度=落入该bin的样本数;freq = FALSE时,y轴是密度(density),柱子高度=频数/(总样本数×bin宽度),此时所有柱子面积之和恒为1。注意:freq = FALSE不等于prob = TRUE(后者是老版本遗留参数,已弃用)。我在教医学统计时,让学生对比hist(cholesterol, freq = TRUE)和hist(cholesterol, freq = FALSE),然后叠加lines(density(cholesterol), col = "red"),他们立刻明白:只有密度直方图才能与核密度曲线在同一尺度下比较平滑度。xlim和ylim:Base R不会自动裁剪“不合理”的数据点。xlim = c(0, 100)强制只显示0–100范围内的数据,但被裁掉的数据仍计入总频数N!这意味着,若你设xlim = c(0,50)而数据有20%落在50以上,那么y轴密度值会整体被拉高25%(因为N不变但有效数据变少)。这在做A/B测试对比时是致命陷阱——我曾见某增长团队用xlim过滤掉“异常高付费用户”,结果对照组直方图峰值虚高,误判活动提升了付费集中度。正确做法是先subset()再绘图,或用xaxs = "i"关闭轴扩展。main,xlab,ylab:看似只是标签,实则是统计沟通的契约。main = "Distribution of Daily Pageviews (n = 1247)"比main = "Pageviews"多传递三个关键信息:变量名、单位隐含(每日)、样本量。我在审计某SaaS公司DAU报表时,发现其Base R脚本里main = "User Activity",但实际数据是session duration(秒),且未标注n,导致下游分析全部基于错误假设。从此我坚持:所有Base R图表的main必须包含“变量+单位+样本量”三要素。
2.3 为什么不用barplot()或plot(table())?它们根本不是直方图
新手常犯的错误是用barplot(table(cut(x, breaks = 10)))来“模拟”直方图。这是危险的伪替代。table(cut())生成的是分类频数表,它丢失了原始数据的连续性信息:cut()默认右闭左开区间,但断点精度受浮点误差影响;barplot()绘制的是离散条形,x轴是因子水平,无法体现bin宽度差异;更重要的是,它完全无法计算密度(因无width信息)。我做过实验:对10万行正态分布数据,用hist(x, breaks = 50, freq = FALSE)和barplot(table(cut(x, 50)), freq = FALSE)分别绘图,前者y轴最大值≈0.4,后者≈1200——因为barplot把密度错误地当成了频数。真正的直方图必须由hist()生成,它是唯一内置intensities计算的函数。其他都是“长得像”的赝品。
3. 核心细节解析与实操要点:参数背后的数学、陷阱与生活化类比
3.1breaks参数的三种形态深度拆解:从“指定数量”到“自适应算法”的决策树
breaks是直方图的灵魂,它的取值直接决定你看到的是数据真相还是噪声幻觉。我们逐层拆解:
第一层:数字型breaks——“我要大约N个柱子”当你写breaks = 30,R并非机械切30等份。它先用Sturges公式计算理论bin数(1 + log₂(N)),再根据数据范围diff(range(x))估算初始bin宽度,最后通过优化算法微调断点位置,确保柱子尽可能等宽且覆盖全范围。例如,对N=1000的数据,Sturges建议约10个bin,但你强制设breaks = 30,R会压缩bin宽度,可能产生大量空柱或单样本柱。实测:x <- rnorm(1000, 0, 1); hist(x, breaks = 30),你会发现两端出现多个高度为1的孤立柱子——这不是数据特征,是过度分箱的 artifact。经验法则:breaks数字不宜超过√N。对1000样本,30已超限;对10万样本,300才是安全上限。
第二层:向量型breaks——“我精确知道哪里该切”这是最可控的方式。breaks = c(0, 10, 20, 50, 100)明确定义4个bin:[0,10)、[10,20)、[20,50)、[50,100]。注意:最后一个区间是闭区间(R默认右闭),若需全左闭,用right = FALSE。这种写法在业务场景中无可替代。例如分析用户年龄:breaks = c(0,18,25,35,45,55,65,Inf),完美对应“未成年”、“青年”、“中年”、“老年”等业务分层。我曾为某教育平台定制报表,要求“18–24岁”和“25–34岁”两个关键客群柱子必须严格对齐,用向量breaks后,运营同学一眼就能比对两组高度,无需查表换算。
第三层:字符串型breaks——“让统计学替我思考”R内置三种经典算法:
"Sturges":k = 1 + log2(n)。假设数据近似正态,对小样本(n<200)效果好。但对长尾数据灾难性——电商GMV数据用它,会把90%的订单挤在第一个bin里。"Scott":h = 3.5 × σ / n^(1/3),σ是标准差。对正态最优,但σ易受异常值污染。处理服务器响应时间(含少量超时毛刺)时,Scott会因σ虚高而生成过宽bin,掩盖主体分布。"Freedman-Diaconis"("FD"):h = 2 × IQR / n^(1/3)。IQR(四分位距)对异常值免疫,是长尾、偏态数据的黄金标准。我处理金融交易额时,FD给出的bin宽度是Scott的1/5,清晰分离出“小额高频交易”和“大额低频交易”双峰。
提示:不要迷信默认。每次画图前,先跑
str(hist(x, plot = FALSE))看breaks字段,对比不同算法生成的断点向量。例如x <- c(rnorm(500, 10, 2), rnorm(50, 50, 5)); hist(x, breaks = "Sturges"); hist(x, breaks = "FD"),你会直观看到FD如何精准捕捉双峰。
3.2freq = FALSE的密度计算:为什么面积=1是检验直方图是否合格的铁律?
当freq = FALSE时,hist()返回的对象中intensities字段的计算逻辑是:intensities[i] = counts[i] / (sum(counts) * diff(breaks)[i])。即每个柱子的高度 = 该bin频数 / (总样本数 × 该bin宽度)。因此,柱子面积 = 高度 × 宽度 = 频数 / 总样本数,所有柱子面积之和 = 总频数 / 总样本数 = 1。
这个“面积=1”是直方图作为概率密度估计器的基石。验证方法极其简单:
h <- hist(x, freq = FALSE, plot = FALSE) sum(h$intensities * diff(h$breaks)) # 应严格等于1(浮点误差内)我曾发现某团队的自动化报表脚本中,因xlim设置不当,sum(...)返回1.23——这意味着他们用错误的N计算了密度,所有后续的KS检验、交叉验证都建立在沙堆上。实操心得:每次用freq = FALSE,必加一行验证代码。更进一步,你可以用此性质做业务校验:若你画的是“用户停留时长(秒)”直方图,且sum(h$intensities * diff(h$breaks)) != 1,说明数据清洗环节漏掉了某些记录,必须回溯ETL流程。
生活化类比:想象一条1公里长的公路(数据范围),上面行驶着1000辆车(样本)。直方图就是把公路分成若干段(bins),每段统计经过的车数。freq = TRUE时,y轴是“每段路上有多少辆车”;freq = FALSE时,y轴是“每米公路上平均有多少辆车”(即车流密度)。后者才能告诉你:在100–200米路段,车流密度是0.005辆/米,意味着平均每200米才有一辆车——这才是对拥堵程度的真实刻画。
3.3xlim与xaxs的协同控制:如何避免“裁剪即失真”的陷阱?
xlim = c(a,b)的作用常被误解为“只画a到b之间的数据”。实际上,它的行为是:保留所有数据用于频数统计,但只绘制x坐标在[a,b]范围内的柱子。这意味着:
- 落在[a,b]外的数据仍计入
sum(counts),从而影响intensities计算; - 若
breaks向量超出[a,b],部分bin会被截断,导致柱子不完整。
例如:x <- c(rnorm(900, 0, 1), rnorm(100, 10, 1)); hist(x, xlim = c(-3,3), breaks = 30),前900个点(均值0)被压缩在-3~3区间,而后100个点(均值10)全被排除在绘图区外,但它们仍参与sum(counts)=1000的计算,导致-3~3区间内柱子密度被系统性压低10%。
解决方案有二:
- 预过滤数据:
x_clean <- x[x >= -3 & x <= 3]; hist(x_clean, breaks = 30)。优点:逻辑清晰,N真实;缺点:丢失了“有10%数据在范围外”的重要业务信号。 - 用
xaxs = "i"关闭轴扩展,并配合breaks向量:hist(x, xlim = c(-3,3), xaxs = "i", breaks = c(-3, seq(-2.5, 2.5, 0.5), 3))。xaxs = "i"强制x轴严格从-3到3,不额外留白;自定义breaks确保所有bin都在范围内,无截断。
注意:
xaxs = "i"会影响所有图形元素(如轴刻度、标签位置),需配合las = 1(水平刻度标签)提升可读性。我在制作合规报表时,强制使用此组合,确保每张图的坐标轴范围100%可复现。
4. 实操过程与核心环节实现:从原始数据到出版级直方图的七步闭环
4.1 第一步:数据诊断——在画图前,先问三个问题
绝不能跳过此步。我见过太多人直接hist(df$col),结果图一片混乱。请务必执行:
x <- df$col # Q1: 数据类型和缺失值? str(x); sum(is.na(x)) / length(x) # 若缺失率>5%,必须决策:删除?插补?标记? # Q2: 数值范围与分布形态? summary(x); fivenum(x) # 关注min/max是否含异常值,中位数vs均值判断偏态 # Q3: 是否存在业务定义的自然分界点? # 如:用户等级(LV1-LV10)、订单状态(0=待支付,1=已支付...)、评分(1-5星) # 这些应优先作为breaks向量实操案例:分析某APP的“单日启动次数”,summary(x)显示Min. = 0.00, 1st Qu. = 1.00, Median = 3.00, Mean = 8.42, 3rd Qu. = 8.00, Max. = 217.00。均值远大于中位数,表明存在长尾。Max=217是真实行为还是数据采集错误?检查日志发现,217次来自一个测试账号,应剔除。最终x_clean <- x[x < 100],再进入下一步。
4.2 第二步:分箱策略决策——基于Q2结果选择breaks
根据诊断结果,选择最匹配的breaks:
- 若
summary()显示近似对称(Mean≈Median),且无极端异常值 →"Sturges"或"Scott" - 若
Max/Min > 100或IQR远小于range(如fivenum中Max=1000, Q3=50)→"FD"或 自定义向量 - 若有明确业务分层(如信用分0-100分,分A/B/C/D四级)→ 向量
c(0,60,70,80,100)
计算示例:对x_clean(启动次数,n=9850),IQR = 7(Q3-Q1),n^(1/3) ≈ 21.4,FD宽度h = 2*7/21.4 ≈ 0.65,范围diff(range(x_clean)) = 99,理论bin数99/0.65 ≈ 152。但152个柱子过于密集,我们折中取breaks = 50,并验证:hist(x_clean, breaks = 50, plot = FALSE)$breaks显示断点间距均匀,符合预期。
4.3 第三步:核心绘图——hist()基础语法与关键参数组合
# 最简可用版(调试用) hist(x_clean, breaks = 50, main = "", xlab = "Daily Launches", ylab = "") # 出版级配置(含密度、颜色、标签) h <- hist(x_clean, breaks = 50, freq = FALSE, # 密度模式,y轴为概率密度 xlim = c(0, 100), # 业务合理范围 xaxs = "i", # 关闭x轴扩展 las = 1, # 水平刻度标签 col = "#4E79A7", # IBM蓝,专业且色盲友好 border = "white", # 白边分离柱子 main = "Distribution of Daily App Launches (n = 9,850)", xlab = "Launches per User per Day", ylab = "Density" ) # 添加均值线(红色虚线) abline(v = mean(x_clean), col = "red", lty = 2, lwd = 2) mtext(paste("Mean =", round(mean(x_clean), 2)), side = 3, line = -2, adj = 0.95, cex = 0.9)关键细节说明:
col = "#4E79A7":非随意选色。IBM设计指南证实此蓝在投影、打印、色弱屏幕上均高辨识,远胜"blue"或"steelblue"。border = "white":白色边框是Base R直方图的“呼吸感”秘诀。它防止柱子粘连,在黑白打印时依然可区分。mtext()添加均值标签:side = 3指顶部,line = -2将标签上提两行,adj = 0.95右对齐,避免遮挡柱子。
4.4 第四步:叠加核密度曲线——用lines()实现分布拟合验证
直方图是离散估计,核密度是连续拟合。二者叠加可验证模型合理性:
# 先画直方图(密度模式) h <- hist(x_clean, breaks = 50, freq = FALSE, xlim = c(0,100), col = "#4E79A7", border = "white", main = "Launch Distribution with Kernel Density Fit", xlab = "Launches", ylab = "Density") # 叠加核密度曲线(蓝色实线) dens <- density(x_clean, from = 0, to = 100, n = 512) # n=512保证平滑 lines(dens, col = "#F28E2B", lwd = 2) # F28E2B是IBM橙,与主色形成专业对比 # 添加图例 legend("topright", legend = c("Histogram", "Kernel Density"), fill = c("#4E79A7", NA), col = c(NA, "#F28E2B"), lwd = c(NA, 2), bty = "n") # bty="n"去掉图例边框,更简洁为什么density()参数要设from/to?density()默认使用range(x),但若x_clean含0值(如未启动用户),from可能为负,导致曲线左延至负数域(无业务意义)。强制from = 0确保物理可解释性。
4.5 第五步:多组对比——用add = TRUE实现严谨的并排分析
Base R不支持facet_wrap,但add = TRUE是更底层、更可靠的对比方式:
# 绘制对照组(iOS用户) hist(ios_launches, breaks = 50, freq = FALSE, xlim = c(0,100), xaxs = "i", col = rgb(0.3, 0.5, 0.7, 0.5), # 半透明蓝色,避免遮挡 border = "white", main = "Launch Distribution: iOS vs Android", xlab = "Launches", ylab = "Density") # 叠加实验组(Android用户),add = TRUE hist(android_launches, breaks = 50, freq = FALSE, xlim = c(0,100), xaxs = "i", col = rgb(0.7, 0.3, 0.5, 0.5), # 半透明红色 border = "white", add = TRUE) # 添加图例 legend("topright", legend = c("iOS", "Android"), fill = c(rgb(0.3, 0.5, 0.7, 0.5), rgb(0.7, 0.3, 0.5, 0.5)), bty = "n")关键优势:add = TRUE确保两组使用完全相同的breaks、xlim、freq设置,消除因参数微小差异导致的视觉偏差。而ggplot2的facet或fill分组,若未显式设定scale = "free_y",y轴会自动缩放,使两组密度不可比。
4.6 第六步:导出与复现——确保图形100%可审计
Base R图形的终极价值在于可复现性。导出必须包含完整环境信息:
# 1. 保存为PDF(出版首选,矢量无损) pdf("launch_distribution.pdf", width = 8, height = 6) hist(x_clean, breaks = 50, freq = FALSE, xlim = c(0,100), xaxs = "i", col = "#4E79A7", border = "white", main = paste("Launch Distribution (n =", length(x_clean), ")"), xlab = "Launches", ylab = "Density") dev.off() # 2. 保存R脚本快照(含环境信息) writeLines(c( "# Generated on:", date(), "# R version:", getRversion(), "# Packages:", paste(names(sessionInfo()$otherPkgs), collapse = ", "), "# Data source: df$col, cleaned on ", format(Sys.time(), "%Y-%m-%d %H:%M:%S"), "", "x_clean <- df$col[df$col < 100]", "hist(x_clean, breaks = 50, freq = FALSE, xlim = c(0,100), xaxs = \"i\", ...)" ), "hist_script.R")为什么PDF优于PNG?
PDF是矢量格式,放大无限清晰,且嵌入字体(如pdf(..., family = "Helvetica")),确保在任何设备上显示一致。PNG是位图,放大后锯齿,且字体渲染依赖本地系统。
4.7 第七步:自动化封装——写一个健壮的my_hist()函数
为避免重复劳动,封装为函数,内置错误检查:
my_hist <- function(x, breaks = "FD", xlim = range(x, na.rm = TRUE), main = NULL, xlab = deparse(substitute(x)), ylab = "Density", col = "#4E79A7", ...) { # 输入验证 if (!is.numeric(x)) stop("x must be numeric") if (length(x) == 0) stop("x is empty") if (all(is.na(x))) stop("x contains only NAs") # 清理缺失值 x_clean <- x[!is.na(x)] if (length(x_clean) == 0) stop("All values in x are NA after cleaning") # 设置默认main if (is.null(main)) { main <- paste("Distribution of", xlab, "(n =", length(x_clean), ")") } # 绘图 h <- hist(x_clean, breaks = breaks, freq = FALSE, xlim = xlim, xaxs = "i", las = 1, col = col, border = "white", main = main, xlab = xlab, ylab = ylab, ...) # 返回hist对象,供后续分析 invisible(h) } # 使用示例 my_hist(df$launches, breaks = 50, xlim = c(0,100))函数设计哲学:不追求“全自动”,而是“防错+可干预”。它强制你思考breaks和xlim,同时拦截常见错误(空数据、全NA),返回hist对象便于后续提取breaks或intensities做统计检验。
5. 常见问题与排查技巧实录:那些文档里不会写的血泪教训
5.1 问题1:“柱子高度为0,但图上显示细线”——浮点精度导致的空bin幻影
现象:hist(x, breaks = 100)后,图中出现大量极细的“毛刺线”,高度几乎为0,但h$counts显示对应bin频数为0。
原因:R在计算breaks时使用浮点运算,diff(h$breaks)可能产生微小误差(如1e-16),导致intensities[i] = 0 / (N * 1e-16)在数值计算中溢出为Inf或NaN,绘图时被渲染为细线。
排查:
h <- hist(x, breaks = 100, plot = FALSE) which(h$counts == 0) # 找出空bin索引 h$intensities[which(h$counts == 0)] # 查看对应intensities值解决:在绘图前过滤掉空bin:
h <- hist(x, breaks = 100, plot = FALSE) # 重置intensities中空bin为0 h$intensities[h$counts == 0] <- 0 # 用barplot手动绘制(更可控) barplot(h$intensities, names.arg = round(h$mids, 2), space = 0, col = "#4E79A7", border = "white", ylim = c(0, max(h$intensities, na.rm = TRUE)))5.2 问题2:“y轴数值巨大,远超1”——freq = TRUE误用为密度图
现象:hist(x, freq = FALSE)后,y轴最大值达500,明显违背“面积=1”原则。
原因:xlim设置过窄,但breaks向量未同步调整。例如x范围是0–1000,你设xlim = c(0,10),而breaks = 50仍试图在0–1000上分50份,导致0–10区间内bin极窄(宽度≈20),intensities = count/(N*width)因width过小而爆炸。
排查:检查diff(h$breaks)的最小值:
h <- hist(x, xlim = c(0,10), breaks = 50, freq = FALSE, plot = FALSE) min(diff(h$breaks)) # 若为0.2,而N=1000,则intensities最大可能达 count/(1000*0.2)=5*count解决:永远让breaks适配xlim:
# 正确做法:先定xlim,再算breaks xlim_range <- c(0,10) breaks_vec <- seq(xlim_range[1], xlim_range[2], length.out = 51) # 50 bins hist(x, breaks = breaks_vec, xlim = xlim_range, freq = FALSE)5.3 问题3:“两组叠加后颜色混成一团”——半透明色设置失效
现象:用rgb(0.3,0.5,0.7,0.5)画第一组,rgb(0.7,0.3,0.5,0.5)画第二组,但叠加后看不出层次,像一团紫色。
原因:Base R的hist()默认使用plot = TRUE,其内部调用polygon()绘制柱子,而polygon()对alpha通道的支持在不同设备上不一致。更可靠的方式是用barplot()手动绘制。
解决:放弃add = TRUE,改用barplot叠加:
# 计算两组hist对象 h1 <- hist(group1, breaks = 50, plot = FALSE) h2 <- hist(group2, breaks = h1$breaks, plot = FALSE) # 强制相同breaks # 创建矩阵,每列一组 mat <- cbind(h1$intensities, h2$intensities) colnames(mat) <- c("Group1", "Group2") # barplot叠加(space=0消除间隙) barplot(mat, beside = FALSE, # 堆叠而非并排 col = c(rgb(0.3,0.5,0.7,0.5), rgb(0.7,0.3,0.5,0.5)), space = 0, names.arg = round(h1$mids, 1), ylim = c(0, max(mat, na.rm = TRUE)))5.4 问题4:“中文标签显示为方块”——字体编码的经典坑
现象:main = "用户启动次数分布"在Windows上显示为□□□□。
原因:Base R默认使用Latin-1编码,中文需显式指定字体族。
解决:在pdf()或png()设备中设置family:
# Windows系统 pdf("hist.pdf", family = "SimSun") # 宋体 # macOS系统 pdf("hist.pdf", family = "Heiti SC") # 黑体-简 # Linux系统(需安装文泉驿) pdf("hist.pdf", family = "WenQuanYi Zen Hei") # 或统一用Cairo设备(跨平台) library(Cairo) CairoPDF("hist.pdf", family = "sans") # Cairo自动处理中文字体5.5 问题5:“图形导出后坐标轴刻度消失”——设备尺寸与字体大小失配
现象:pdf("hist.pdf", width=5, height=4)后,x轴刻度