报错一出脚本当场停住满屏红字里最刺眼的是这一句Error in plot.xy(xy.coords(x, y), type type, ...) : plot.new has not been called yet。我第一次在SVM-RF绘图的流程里撞上它时第一反应是“我明明前面画过好几张图了怎么会说还没调用plot.new”。后来把R图形设备的工作方式翻了个底朝天才发现这个报错和“你画没画过图”没那么大关系真正要紧的是“当前图形设备上有没有一个活动中的绘图页面”。这篇文章就从这个报错出发把SVM-RF这类建模绘图场景里最容易被忽略的图形设备机制、排查顺序和修法一次说清楚。1. 报错文本在R图形系统里代表什么plot.new、plot.xy和xy.coords的三方关系1.1 R把绘图函数分成“开页”和“叠加”两类而这个报错刚好卡在两类之间R的graphics包表面上是一套绘图函数底层其实有一个非常简单的分工逻辑。像plot()、barplot()、hist()、boxplot()这一类高级绘图函数它们的主线流程是先调plot.new()在当前图形设备上开启一个全新的绘图页面然后调plot.window()建立坐标系最后再用一系列低层绘图函数把数据画上去。换句话说高级绘图函数自带“开新页”的动作。另一类则是points()、lines()、abline()、text()、legend()、axis()、segments()这些低级绘图函数。它们的定位就是“在当前已经存在的绘图页面上继续叠加内容”所以设计上根本不会去调用plot.new()。这也正是它们能实现给一张图加注、加线、加图例的原因。问题就出在这儿低级绘图函数默认“当前设备上一定有一个已经开始、且还没结束的绘图页面”。如果当前设备上压根没有这个页面或者上一张图已经被结束掉了那这些函数往里叠东西就失去了落点。plot.xy是points()和lines()内部共同依赖的底层函数它负责把规范化后的坐标真正画到设备上。xy.coords()则是把用户传入的x、y各种形态的数据统一转换成坐标结构。一句话概括这个报错你试图在“一张还不存在的图”上落笔画点R拒绝执行。1.2 为什么报错信息里有三个函数名却看不到你自己的代码很多初学者看到plot.xy(xy.coords(x, y), type type, ...)会以为是自己传参传错了到处都是英文就是没看到自己写的函数名。其实这是R的报错定位习惯它抛错的位置在底层的plot.xy而不是在你调用的points()或lines()那一行。traceback()能帮你把调用链倒出来一般最外层就是你脚本里的那一行低级绘图调用。举个最简单的复现例子你随便打开一个干净的R会话直接运行points(1:5, 1:5)只要当前设备上没有任何活动绘图页这个报错就会出现。同样的道理如果你先画了一张图然后手动执行了dev.off()把页面关掉再执行points()也会收到同一句红字。所以这个报错的本质和“数据对不对”无关它纯粹是设备状态问题。2. SVM-RF绘图里的三个高发触发点重要性图、决策边界、多曲线叠加2.1 随机森林特征重要性图barplot被条件分支跳过后续text照画不误SVM-RF这个组合通常是先用随机森林做特征筛选再用SVM建模分类。随机森林那一环最常见的是画特征重要性条形图。很多人喜欢先barplot()把重要性画出来再拿它的返回值当坐标基准用text()把数值标到柱子尾部。表面上看这没什么问题barplot()是高级绘图函数确实会开新页面text()只是叠加。但如果这段代码被塞进了某个条件分支里事情就变了if (rf_result$importance_status ok) { imp - as.data.frame(randomForest::importance(rf_model, scale TRUE)) bp - barplot(imp$MeanDecreaseAccuracy, horiz TRUE, col steelblue) } # 条件不满足时barplot这一整块没执行 # 但下面这行没有“保护”照样运行 text(x imp$MeanDecreaseAccuracy, y bp, labels round(imp$MeanDecreaseAccuracy, 2))当importance_status变成其他值时barplot()根本没跑当前设备上自然没有任何活动页面text()一执行就触发plot.new has not been called yet。这个坑的关键不在于“图有没有画过”而在于“画图的动作有没有真的执行到”。长脚本里前面的代码被if跳过、被循环的某次迭代跳过、被提前return掉都是很常见的。2.2 SVM决策边界图先预测网格再叠加散点漏一步底图就全盘崩SVM分类模型画决策边界已经几乎是建模流程里的固定动作了。标准做法是把特征空间切成一个网格让模型对每个网格点做预测然后把不同预测类别的点用不同颜色画出来形成背景色块再叠加上原始样本点和支持向量。问题在于很多人写这段代码时习惯先算网格预测再直接points()画背景色svm_fit - e1071::svm(y ~ ., data train_data, type C-classification, kernel radial) x_grid - seq(min(train_data$x1), max(train_data$x1), length.out 100) y_grid - seq(min(train_data$x2), max(train_data$x2), length.out 100) grid_data - expand.grid(x1 x_grid, x2 y_grid) pred_grid - predict(svm_fit, grid_data) # 如果漏了这一步底图下面的points必然报错 # plot(1, type n, xlim range(x_grid), ylim range(y_grid), ...) points(grid_data, col c(lightyellow, lightblue)[as.integer(pred_grid)], pch 16, cex 0.6)网格动辄几千上万个点这个points()的数据量很吓人但哪怕它数据量再大也只是个低级绘图函数。它不会自己开页面也不会自己建坐标系。你漏了上面那句plot(1, type n, ...)它就一定报错。相反如果你用的是plot(svm_fit, data train_data)这种直接画模型对象的方式因为它内部走的是plot()的分发机制一般会自动开页面倒不太容易踩这个雷。2.3 多曲线ROC叠加pROC的addTRUE参数依赖一幅“已经存在的底图”SVM-RF流程最后常要画ROC曲线来对比随机森林和SVM的分类效果。pROC包的plot.roc()有个特别容易让人误用的设计第一次画曲线时它会自动开新页面所以你觉得“这个函数很安全”但当你想叠加第二条曲线时必须传add TRUE这时候它内部调用的就是低级叠加逻辑底图必须已经存在。library(pROC) roc_rf - roc(response test_y, predictor rf_pred_prob) roc_svm - roc(response test_y, predictor svm_pred_prob) plot.roc(roc_rf, col black, lwd 2) # 安全会自动开页 plot.roc(roc_svm, add TRUE, col red) # 如果上一句没执行这里就炸上面最后一行如果前面那行plot.roc(roc_rf, ...)因为某个条件判断没走到或者你的脚本里设备已经在前一步被dev.off()关掉叠加第二条曲线时就会碰到plot.new has not been called yet。加上很多人还会顺手用legend()、text()标AUC这些同样都是叠加类函数一个底图不稳后面一串全崩。2.4 还有一个反直觉的触发方式par(mfrow)只改布局不自动打开任何面板par(mfrow c(1, 2))想把两张图并排放一起也容易埋雷。注意par()只是设置“接下来新建页面时分几行几列”它本身不会替你创建任何绘图面板。你必须在每个面板里至少调用一次高级绘图函数或者至少调用一次plot.new()面板才会真正被建立。我见过这么写的par(mfrow c(1, 2)) plot(importance_plot) # 第一个面板有了 # 第二个面板想手动控制坐标系 plot.window(xlim c(0, 1), ylim c(0, 1)) lines(0:1, 0:1, col red)如果少了第二面板里的plot.new()plot.window()自己也不会开页面它只是设置当前第一个页面的坐标系紧接着的lines()叠加倒是不会报错但画的内容会叠到第一张图上后面的逻辑全乱。反过来如果连plot()都没执行就直接lines()那就又是这段红字。总之par(mfrow)本身救不了你每个面板都要有“开新页”的动作。3. 报错之后我按什么顺序排查从调用栈到设备状态的完整链路3.1 第一步让报错现场多留点线索别急着改代码遇到这个报错我的习惯是先别动代码而是立刻执行traceback()看R记录的调用栈。它能直接告诉我报错是从哪个函数传到plot.xy里的通常最外层就是我脚本里的那一行。我自己踩坑时十次里有七次是脚本里一个不起眼的text()或者points()引起的光看报错正文很难定位但traceback()一眼就能看到入口函数。如果还想更彻底地进入现场可以临时设置options(error recover)。这样报错发生时R会弹出一个菜单列出所有被调用函数的运行环境你可以逐个环境查看当时的变量值、设备状态等于把“案发现场”完整保留下来。排查完记得改回options(error NULL)。3.2 第二步检查当前图形设备和“活动页面”状态设备状态是核心。在R会话里执行下面几个函数dev.list() # 列出当前所有打开的图形设备 dev.cur() # 当前正在使用的设备及其编号重点看两件事。第一当前设备是什么设备。如果你刚才跑过png()或pdf()开了文件设备又没及时dev.off()那么你后续的低级绘图操作会全部写到文件设备上。如果这个文件设备打开后还一个页面都没建那points()必然报这句错。第二当前设备上有没有活动绘图页。dev.list()能看到的只是设备本身它不代表设备上已有页面页面是由plot.new()建立并由后续低级绘图函数共享的。举个现场例子dev.new() # 打开一个新设备 plot(1:10) # 在这个设备上画了图 dev.off() # 把这个设备关掉了当前设备切回之前的交互设备 points(5, 5) # 报错plot.new has not been called yet这个例子特别典型因为它完美展示了“画过图”和“当前页面存在”的区别。你确实画过但dev.off()把那个带页面的设备关掉之后当前设备的页面状态被清空了再叠加低级图形时R就只认当前设备的状态。3.3 第三步审查所有高级绘图调用是否真的被执行了设备没问题的话下一步就要回到脚本逻辑里找把高级绘图函数执行的真实路径捋一遍。重点排查三类情况。第一if分支。看barplot()、plot()这些“开页”函数是不是被包在条件判断里条件为FALSE时整段被跳过。第二for循环。看循环次数是不是可能为0或者循环被next、break中断过导致“开页”那一步没走。第三自定义函数。我之前在SVM-RF流程里封装过一个画特征选择曲线的函数函数入口直接就是plot()建底图本来没问题但后来为了复用加了add参数结果某个调用路径上add TRUE而函数外部又没人先建底图报错就来了。这类问题用browser()或者在关键绘图语句前后加print(reached here)都能快速判定。不要靠眼睛盯代码长脚本里人眼很容易漏掉执行路径。3.4 第四步确认是不是面板布局留了空位如果你用了par(mfrow)或者layout()还要单独核对布局里的每个区域是否都被高级绘图函数“点亮”过。layout()手动布局的规则和par(mfrow)一样每个区域都要先有一个页面的动作才能在里面叠加内容。如果一个区域里只写了lines()或text()没有先画任何底图那么当R“翻到”那个空区域去执行低级绘图操作时照样报这句错。排查时可以直接在出错的调用前面临时加一句plot.new()如果加了之后不再报错说明就是某个面板缺了开页动作。这个方法用来“诊断”比直接改代码快得多。4. 四种修复写法与各自的适用边界4.1 最朴素的修法显式调用plot.new再用plot.window建坐标系如果报错原因就是“当前设备上没有页面”那最直接的修法是手动补上开页动作plot.new() plot.window(xlim c(0, 10), ylim c(0, 10)) box() axis(1) axis(2) points(x 1:5, y 1:5)这里的plot.new()负责把现有设备上的旧图清掉并开启一个新页面plot.window()随后建立坐标系。注意plot.window()不会画坐标轴、不会画边框这些都要自己用box()、axis()、title()补上。如果你用plot(1, type n)一步到位它会把开页和坐标系都做了所以我个人其实更推荐下面第二种写法。4.2 最推荐的修法用plot(type n)建“空底图”后续任意叠加对于SVM决策边界、ROC曲线叠加这种有大量后续元素的场景最稳的写法是先把底图画成一张“空图”plot(1, type n, xlim c(-3, 3), ylim c(-3, 3), xlab Feature 1, ylab Feature 2, main SVM Decision Boundary) # 这时坐标系已经建立随便叠加 points(grid_data, col c(lightyellow, lightblue)[as.integer(pred_grid)], pch 16, cex 0.6) points(train_data[, 1:2], col train_data$y, pch 19)plot(1, type n)虽然是高级绘图函数但它不画任何数据点只建立了一个空白绘图区域相当于画好了一张“底图”。之后所有的points()、lines()、abline()、text()、legend()都可以放心往上面叠加。这种写法在SVM-RF流程里是最不容易出错的。4.3 封装成带add参数的绘图函数一劳永逸如果同一个图要在脚本里被反复画比如SVM-RF对比实验里要为不同训练集画决策边界我建议直接封装一个带add开关的函数把“要不要建底图”这个问题固化下来plot_decision_boundary - function(model, data, grid, add FALSE, ...) { if (!add) { plot(1, type n, xlim range(grid[[1]]), ylim range(grid[[2]]), xlab names(grid)[1], ylab names(grid)[2], ...) } pred - predict(model, grid) points(grid, col c(lightyellow, lightblue)[as.integer(pred)], pch 16, cex 0.6) points(data[, 1:2], col data[[3]], pch 19) invisible(grid) }调用的时候第一次用add FALSE建底图后续叠加别的模型结果时用add TRUE。这样做的好处是低级绘图函数永远被关在“至少建过一次底图”的函数逻辑里外面怎么调用都不容易踩空。我在自己的R脚本里凡是跟SVM-RF相关的绘图都采用了这个模式后来基本没再见过这句报错。4.4 从设备管理层面治本文件设备成对开闭不随手dev.new很多触发这个报错的场景问题根源根本不在某一行绘图代码而在于设备管理散乱。最典型的是png(output/rf_importance.png) # 打开文件设备 # 画图代码... # 忘了 dev.off() # 接下来在交互设备上继续画图 # 但所有图都被写进文件设备里并且文件设备还没有任何页面如果画图流程里某个低级绘图函数出现在了“文件设备刚打开但还没建页面”的位置就一定会报错。所以我的经验是只要用png()或pdf()开设备就一定保证“打开—绘制—关闭”成对出现中间不故意插dev.new()、dev.off()去切设备。一个干净的环境能让报错概率直线下降。4.5 简单修法的风险提示不要无脑在报错前加plot.new有一种看起来很快但后患很大的修法看到报错随手在出错那一行前面加一句plot.new()。如果是单个脚本、只画一张图确实能蒙混过关但如果你的脚本后面还要叠加更多内容或者这段代码被封装进函数里多次调用那么这会导致每调用一次就强制开一个新页面前一张图被“翻页”清掉叠加类图例、曲线全被冲掉。所以plot.new()应该作为“明确要新建页面”的意图去使用而不是作为“让报错消失”的手段。更好的办法是回到第4.2节的空底图写法先把整张图的坐标系和绘图区域规划清楚。5. 同类图形报错的识别与三条防复发习惯5.1 和它经常一起出现的其他图形设备报错R的图形系统问题不止这一种我在SVM-RF绘图流程里还踩过另外几个相近的坑。分清它们的区别排查起来会快很多。报错信息本质原因和本文报错的区别plot.new has not been called yet当前设备没有活动绘图页低级绘图函数落在了空页面上invalid graphics state设备内部状态不一致可能是设备被非正常关闭、页面和坐标状态错乱figure margins too large绘图区域尺寸小于默认边距要求属于布局尺寸问题不是页面缺失no applicable method for plot对象没有对应plot方法高级绘图分发失败通常和SVM模型对象有关尤其要注意figure margins too large它经常被误判成和本文报错同一个问题。其实它说的是设备窗口或者文件图片的宽度不够放不下par()设置的边距导致绘图区域变成负值。这在交互设备上拖拽窗口时很容易碰到画布调大或者调整par(mar)就能解决。5.2 需要“叠加到底图上”的低级绘图函数清单为了后面不再频繁踩坑我把R里那些“默认必须有活动绘图页”的常用函数列出来。看到这些函数的时候先问自己一句当前底图建好了吗points()、lines()、abline()、segments()、arrows()、rect()、polygon()text()、mtext()、legend()、axis()、box()rug()、curve(..., add TRUE)、lines(..., type l)pROC包里的plot.roc(..., add TRUE)各种包中以add TRUE为参数的叠加类函数这些函数本身不会主动开新页面使用前必须保证当前设备上已经有一次plot.new()或高级绘图调用建立了活动绘图页。5.3 三条我自己实测有效的防复发习惯第一条绘图代码尽量写在函数里并且在函数入口统一处理“底图是否存在”的问题。比如我上面写的add参数模式或者if (!add) plot(1, type n, ...)。让普通调用永远从add FALSE开始。第二条长脚本开始运行前先执行一次graphics.off()把之前残留的设备全部清掉。很多人开着R会话连续跑脚本上一个脚本没关掉的png()设备会影响下一个脚本的设备状态这几乎是“莫名报错”的第一来源。第三条如果实在担心某个低层叠加操作会落在空页面上可以在叠加以前用一个条件判断if (any(dev.list() dev.cur())) { # 当前设备是存在的但是否有活动页面不好说仍然建议先建底图 }不过这只能判断设备存不存在判断不了活动页面是否存在。真正保险的还是那句话底图优先。遇到这句报错我的第一反应永远是“当前这台图形设备上有没有一张正在画的图”。想清楚这一个问题比瞎改二十行代码都管用。SVM-RF流程本身不难图也都不复杂把图形设备这个底层机制理顺了这类报错就算彻底翻篇了。