軸范圍設(shè)置:從xlim/ylim到coord_cartesian的完整指南)
坐標(biāo)軸范圍大概是數(shù)據(jù)可視化里最容易被當(dāng)成“小事”的參數(shù)了。很多人畫圖時不看默認(rèn)的xlim和ylim直到圖出來才發(fā)現(xiàn)主體數(shù)據(jù)被一個離群點壓成一條線或者兩組曲線因為坐標(biāo)范圍不一致根本沒法放在一起比。這篇文章就把“使用xlim(x1,x2)設(shè)定橫坐標(biāo)范圍、使用ylim(y1,y2)設(shè)定縱坐標(biāo)范圍”這件事徹底講透怎么用、什么時候必須用、以及比參數(shù)本身更重要的——修改坐標(biāo)范圍到底會不會改變統(tǒng)計結(jié)果。這個主題適合三類人剛開始用R畫圖、總被默認(rèn)坐標(biāo)軸坑到的初學(xué)者能用ggplot2和base繪圖但說不清xlim與coord_cartesian區(qū)別的進(jìn)階用戶以及經(jīng)常做數(shù)據(jù)匯報、需要讓圖表更“誠實”的分析師。我盡量用實際操作說話每個結(jié)論都對應(yīng)一段可復(fù)現(xiàn)代碼。1. 坐標(biāo)范圍是數(shù)據(jù)可視化的“取景框”1.1 自動坐標(biāo)范圍到底是怎么算出來的先問一個很多人沒想過的問題不寫xlim和ylim時R到底怎么決定坐標(biāo)軸范圍答案是它并不直接取數(shù)據(jù)的最小值和最大值而是取一個“看起來舒服”的擴(kuò)展區(qū)間。base R里默認(rèn)的坐標(biāo)軸風(fēng)格是xaxsr、yaxsr。這個“r”代表range意思是R會在數(shù)據(jù)最小值、最大值的基礎(chǔ)上各向外擴(kuò)展約4%的留白。舉個例子如果x數(shù)據(jù)是0到10默認(rèn)橫軸范圍不會是精確的0到10而大約是-0.4到10.4。這個設(shè)計的本意是防止散點或柱子貼到坐標(biāo)軸上視覺上更透氣。但你發(fā)現(xiàn)問題了嗎自動范圍完全由數(shù)據(jù)決定而不是由你想表達(dá)的信息決定。數(shù)據(jù)里有一個異常大值坐標(biāo)范圍就被拉得很寬剩下的99個點全擠在一個小角落里。數(shù)據(jù)分布范圍是0到1中間有個1000默認(rèn)圖出來主體部分看起來就是一條貼在x軸上的直線。這不是畫圖錯誤是“取景框”選錯了。1.2 四種必須手動調(diào)整坐標(biāo)范圍的場景根據(jù)我自己的使用經(jīng)驗手動設(shè)置xlim/ylim基本逃不出下面四種場景場景一剔除離群點對視野的污染。數(shù)據(jù)里有極端值但你關(guān)心的主體區(qū)域在正常范圍內(nèi)這時把坐標(biāo)范圍限定到合理區(qū)間主體數(shù)據(jù)立刻“顯形”。場景二局部放大。數(shù)據(jù)整體跨度很大你想單獨看0到1區(qū)間、或者50到60區(qū)間的細(xì)節(jié)就需要用xlim把橫軸裁到對應(yīng)范圍。場景三多圖對比時統(tǒng)一坐標(biāo)。兩張圖放在一起如果各自的坐標(biāo)范圍不同視覺上的“高低差異”可能完全是一種錯覺。固定坐標(biāo)范圍后才能看出真正的相對差異。場景四讓圖形符合閱讀預(yù)期。柱狀圖要從0開始否則柱子高度的比例會誤導(dǎo)讀者折線圖反而經(jīng)常裁剪掉零基線用來放大波動的細(xì)節(jié)。這些都需要手動設(shè)定。2. 從參數(shù)到函數(shù)xlim/ylim的兩種打開方式2.1 base R繪圖函數(shù)里的xlim/ylim參數(shù)在base R里xlim和ylim通常是繪圖函數(shù)的參數(shù)不是獨立函數(shù)。最常見的寫法是plot()x - runif(50, 0, 10) y - x rnorm(50, 0, 2) # 默認(rèn)坐標(biāo)范圍 plot(x, y) # 手動設(shè)定橫軸0到10縱軸-5到15 plot(x, y, xlim c(0, 10), ylim c(-5, 15))注意參數(shù)的格式必須是一個長度為2的數(shù)值向量即c(下限, 上限)。ggplot2里可以寫成xlim(0, 10)這種函數(shù)調(diào)用的形式但base R里寫成xlim(0, 10)會報錯因為plot()的xlim參數(shù)期待一個向量。hist()、boxplot()、barplot()、stripchart()等高級繪圖函數(shù)也都接受xlim和ylim參數(shù)。有一個小技巧如果你只想固定下限或上限另一側(cè)可以用Inf。比如xlim c(0, Inf)表示橫軸從0開始上限交給R自動決定這在把坐標(biāo)軸對齊到零點時很常用。還有一個冷知識指定了xlim之后base R默認(rèn)仍然會做4%的擴(kuò)展。也就是說plot(x, y, xlim c(0, 10))畫出來的橫軸并不是精確的0到10而是大約-0.4到10.4。如果你必須讓坐標(biāo)軸嚴(yán)格落在0和10上要加上xaxs i和yaxs i。這個“i”是internal的意思表示坐標(biāo)軸精確貼合指定范圍。plot(x, y, xlim c(0, 10), ylim c(-5, 15), xaxs i, yaxs i)2.2 ggplot2中的xlim()和ylim()函數(shù)在ggplot2里xlim和ylim是真正的函數(shù)而且有兩種使用方式library(ggplot2) df - data.frame(x runif(50, 0, 10), y runif(50, 0, 10)) # 方式一連續(xù)兩個參數(shù) ggplot(df, aes(x, y)) geom_point() xlim(0, 10) # 方式二傳入一個長度為2的向量 ggplot(df, aes(x, y)) geom_point() xlim(c(0, 10))這兩種寫法完全等價。xlim(0, 10)本質(zhì)上是對x軸scale的快捷方式等同于scale_x_continuous(limits c(0, 10))。ylim同理。但這里藏著一個關(guān)鍵區(qū)別很多人在這里踩坑ggplot2的xlim()會直接丟棄范圍之外的數(shù)據(jù)行然后再做統(tǒng)計和繪圖。也就是說geom_smooth()的擬合線、geom_density()的密度曲線都是基于截取后的子集重新計算的。如果你只想“放大看”完整數(shù)據(jù)里的某個局部而不是重新分析子集xlim()不是最好的選擇應(yīng)該用coord_cartesian(xlim c(...))這個區(qū)別我后面專門講。2.3 低層控制刻度軸與畫布邊界base R里還有一種進(jìn)階玩法不滿足于“設(shè)定范圍自動刻度”而是自己控制刻度的位置和標(biāo)簽。方法是先畫圖然后調(diào)用axis()函數(shù)。set.seed(1) x - runif(100, 0, 100) y - x rnorm(100, 0, 10) plot(x, y, xlim c(0, 100), ylim c(0, 120), xaxt n) axis(1, at c(0, 25, 50, 75, 100), labels c(0, 25, 50, 75, 100))xaxt n表示先隱藏默認(rèn)的x軸刻度然后你用axis()自己指定刻度線的位置。這在做論文圖、報告圖時特別有用因為R自動選的刻度經(jīng)常不符合出版要求比如你想要0、20、40、60、80、100這樣的刻度R默認(rèn)可能給出0、25、50、75、100。另外低層繪圖函數(shù)lines()、points()、abline()本身沒有xlim參數(shù)它們畫線時不關(guān)心坐標(biāo)范圍畫完超出邊框的內(nèi)容會被直接裁剪掉。想讓某些元素不受坐標(biāo)范圍限制可以設(shè)置par(xpd TRUE)這樣圖形可以畫到繪圖區(qū)域外面去處理圖例、標(biāo)注時經(jīng)常用到。3. 三個實戰(zhàn)場景坐標(biāo)范圍該這么設(shè)3.1 裁剪離群點讓主體數(shù)據(jù)顯形先構(gòu)造一個典型場景100個點均勻分布在0到1之間再加一個離群點1000。set.seed(123) x - c(runif(100, 0, 1), 1000) y - c(runif(100, 0, 1), 1000) # 默認(rèn)圖主體數(shù)據(jù)被壓成左下角一團(tuán) plot(x, y) # 手動設(shè)定范圍看清主體 plot(x, y, xlim c(0, 1.2), ylim c(0, 1.2), pch 16, col steelblue)不設(shè)置坐標(biāo)范圍時x軸會跑到1000附近0到1之間的100個點擠得連形狀都看不出來。設(shè)置xlim和ylim后離群點被“擠出”視野剩下的點分布在屏幕中央規(guī)律一目了然。但我必須提醒這里只是“視野上”忽略了離群點數(shù)據(jù)本身還在。后續(xù)如果做回歸、算相關(guān)系數(shù)1000這個值仍然會參與計算。坐標(biāo)范圍和數(shù)據(jù)分析是兩個維度的事不要用畫圖參數(shù)代替數(shù)據(jù)清洗。3.2 固定坐標(biāo)范圍讓多圖對比不騙人做分組對比時最忌諱的就是每張子圖的坐標(biāo)范圍各自為政。人的視覺對“相對位置”極其敏感如果A組的圖縱軸是0到50B組的圖縱軸是0到100B組數(shù)據(jù)在視覺上會被“壓縮”看起來比A組平緩很多但這不是數(shù)據(jù)的真實差異而是坐標(biāo)范圍造成的錯覺。看一個最樸素的例子set.seed(2024) a - rnorm(50, mean 5, sd 1) b - rnorm(50, mean 6, sd 1) par(mfrow c(1, 2)) plot(a, ylim c(0, 10), main A組) plot(b, ylim c(0, 10), main B組) par(mfrow c(1, 1))兩幅圖都限定ylim為0到10A組和B組之間的均值差異就可以直接通過點的高低比較。如果讓R自動決定坐標(biāo)范圍A組可能顯示為0到8B組顯示為2到10粗看會以為兩組波動程度差距很大實際上標(biāo)準(zhǔn)差幾乎一樣。ggplot2里對應(yīng)的操作是facet_wrap()或facet_grid()中的scales參數(shù)。scales fixed默認(rèn)值就是所有子圖共用同一個坐標(biāo)范圍這是對比圖的安全選擇scales free則允許每個子圖自己決定范圍用于數(shù)據(jù)量級差異過大、放在一起根本看不見的情況。我的經(jīng)驗是優(yōu)先fixed只有確實有子圖被壓扁時才用free。3.3 局部放大同一組數(shù)據(jù)的不同區(qū)間有一類場景很常見數(shù)據(jù)從0到100你既想看頭部0到10的細(xì)節(jié)又想看中間40到60的趨勢。用subset篩數(shù)據(jù)再畫圖當(dāng)然可以但有一個問題只要數(shù)據(jù)一變映射到坐標(biāo)軸上的擬合線、置信區(qū)間都會基于子集重新計算已經(jīng)不是原始數(shù)據(jù)在這些區(qū)間上的表現(xiàn)了。更穩(wěn)妥的做法是畫兩次原數(shù)據(jù)圖只改xlimset.seed(10) x - seq(0, 100, length.out 200) y - sin(x / 10) rnorm(200, 0, 0.1) # 看0到10區(qū)間 plot(x, y, xlim c(0, 10)) # 看40到60區(qū)間 plot(x, y, xlim c(40, 60))這樣能保證兩次展示的是同一份數(shù)據(jù)、同一個坐標(biāo)系只是“取景框”不同。你會發(fā)現(xiàn)0到10區(qū)間里的鋸齒波動在完整視圖里根本看不出來。局部放大不是為了隱瞞什么而是讓讀者注意到整體圖上被淹沒的細(xì)節(jié)。這個需求在ggplot2里同樣建議配合geom_smooth一起理解。如果你用xlim(40, 60)擬合線只基于40到60的數(shù)據(jù)重新算如果你用coord_cartesian(xlim c(40, 60))擬合線仍然基于全部200個點只是觀察窗口縮小了。后者才真正做到“放大同一份數(shù)據(jù)”。4. 五個坐標(biāo)軸范圍的坑每個我都踩過4.1 xlim截斷數(shù)據(jù)coord_cartesian縮放視圖這是ggplot2用戶最容易踩的坑我單獨放在第一個講。很多人的直覺是xlim(0, 10)就是把橫軸窗口拉近圖形內(nèi)容不變。但實際上xlim()先刪除范圍外的數(shù)據(jù)再執(zhí)行幾何對象的計算最后才畫圖。一個立即產(chǎn)生影響的例子是geom_smooth()library(ggplot2) set.seed(42) df - data.frame(x 1:100, y sin(1:100 / 10) rnorm(100, 0, 0.2)) # 效果一先截數(shù)據(jù)再擬合置信區(qū)間基于20到80的子集 ggplot(df, aes(x, y)) geom_point() geom_smooth() xlim(20, 80) # 效果二完整數(shù)據(jù)擬合只是視圖放大到20到80 ggplot(df, aes(x, y)) geom_point() geom_smooth() coord_cartesian(xlim c(20, 80))第一張圖的平滑曲線在窗口邊緣可能會出現(xiàn)明顯彎曲甚至趨勢反轉(zhuǎn)因為邊界附近的數(shù)據(jù)被截掉了一部分?jǐn)M合樣本不完整第二張圖才是“原數(shù)據(jù)的局部放大”平滑趨勢與全圖完全一致。判斷一張圖是否被截斷還有個簡單方法看置信區(qū)間在邊界處是不是突然收窄如果是多半是數(shù)據(jù)被截斷了。應(yīng)用原則只想讓眼睛看到局部用coord_cartesian想真正基于局部子集重新統(tǒng)計再用xlim()。4.2 hist的組距不會跟著xlim走改xlim對散點圖、折線圖基本只是視野問題但對直方圖會有隱藏影響。hist()的組距breaks默認(rèn)基于完整數(shù)據(jù)范圍來計算即使你在hist()里設(shè)置了xlimR也是先用完整數(shù)據(jù)算好組距然后只顯示范圍內(nèi)的部分柱子。看這段代碼set.seed(7) d - c(rnorm(1000, 0, 1), runif(10, 50, 60)) hist(d, xlim c(-5, 5))你預(yù)期看到0附近的分布但因為數(shù)據(jù)里混了10個50到60之間的值R默認(rèn)把breaks鋪到很寬的范圍每個組距被拉大導(dǎo)致-5到5區(qū)間里的柱子只有幾根根本看不清分布形狀。正確做法是先篩掉離群點或者手動指定breakshist(d[d -5 d 5], breaks 50, xlim c(-5, 5))或者對全量數(shù)據(jù)指定足夠細(xì)的組距再限制xlimhist(d, breaks seq(-60, 60, by 0.2), xlim c(-5, 5))記住一句話直方圖里xlim只影響“展示哪一段”不影響“怎么分組”。想讓分組合理得先把breaks控制好。4.3 日期軸、因子軸不能直接填數(shù)字很多人在處理時間序列時直接寫xlim c(20240101, 20241231)結(jié)果圖形空白或者報錯。原因是R里的日期是一個Date對象底層雖然是數(shù)字但坐標(biāo)軸的limits要求傳入相同類型的值。base R里date軸的正確寫法是dates - seq(as.Date(2024-01-01), as.Date(2024-12-31), by day) vals - cumsum(rnorm(length(dates))) plot(dates, vals, xlim as.Date(c(2024-01-15, 2024-03-01)), type l)ggplot2里處理時間序列時可以在scale層做library(ggplot2) df - data.frame(date dates, value vals) ggplot(df, aes(date, value)) geom_line() scale_x_date(limits as.Date(c(2024-01-15, 2024-03-01)))注意連續(xù)兩個月數(shù)據(jù)本來有366個點xlim這樣一寫范圍外的數(shù)據(jù)點并不會顯示。但這和4.1節(jié)說的一樣xlim會截斷數(shù)據(jù)如果只是放大看也得用coord_cartesian(xlim as.Date(c(...)))。因子軸也有自己的坑。假設(shè)x軸是因子型變量包含“低”“中”“高”三個水平直接xlim(低, 高)在ggplot2里是可以的但如果你限定的值不在因子水平里會報錯或出空白。離散軸的limits本質(zhì)是“允許顯示哪些類別”而不是“數(shù)值邊界”理解這一點就不會迷惑了df2 - data.frame(group factor(c(低, 中, 高), levels c(低, 中, 高)), value c(1, 2, 3)) ggplot(df2, aes(group, value)) geom_col() scale_x_discrete(limits c(低, 高))這樣畫出來只有一個“低”柱和一個“高”柱“中”被篩掉了。注意順序limits里的順序就是軸上類別的順序。4.4 對數(shù)坐標(biāo)軸的邊界選擇用log x或者scale_x_log10()時坐標(biāo)范圍的下限不能是0或負(fù)數(shù)。對數(shù)在0處沒有定義xlim c(0, 100)在普通坐標(biāo)下沒問題但一旦取對數(shù)R會抱怨或者畫出一片空白。x - seq(1, 100, length.out 100) y - x^0.5 # 正常 plot(x, y, log x, xlim c(1, 100)) # 報錯或圖像異常0不能取對數(shù) plot(x, y, log x, xlim c(0, 100))ggplot2同理。使用scale_x_log10(limits c(...))時下限一定要大于0。如果你處理的數(shù)據(jù)本身包含0常見做法是先加一個很小的偏移量比如x 0.1再取對數(shù)。還有一種情況對數(shù)坐標(biāo)下要求坐標(biāo)軸刻度對齊某些特定數(shù)字比如1、10、100、1000可以在limits里精確指定邊界再讓breaks也對應(yīng)這些值圖看起來會非常規(guī)整。4.5 坐標(biāo)軸標(biāo)簽被裁掉怎么辦設(shè)定坐標(biāo)范圍后偶爾會出現(xiàn)圖邊緣的刻度標(biāo)簽被截掉一半的情況。原因通常是圖形設(shè)備尺寸有限而坐標(biāo)范圍邊界上的文字、標(biāo)簽超出了繪圖區(qū)域。最簡單的處理是給繪圖區(qū)域留出更多外部邊距。base R里用par(mar c(bottom, left, top, right))par(mar c(5, 5, 2, 2)) plot(x, y, xlim c(0, 1.2), ylim c(0, 1.2))ggplot2則可以通過theme()調(diào)整plot.marginggplot(df, aes(x, y)) geom_point() coord_cartesian(xlim c(0, 10)) theme(plot.margin margin(1, 2, 1, 1, cm))還有一種常見情況你在圖上用text()或annotate()添加了額外標(biāo)注標(biāo)注點正好落在坐標(biāo)范圍邊界外被裁掉了??梢杂胮ar(xpd TRUE)允許繪制超出區(qū)域或者干脆把ylim略微擴(kuò)大一點給標(biāo)注留位置。我的習(xí)慣是統(tǒng)一“先設(shè)定核心范圍再給標(biāo)簽和注釋留10%的余量”這樣既保證主體清晰又不會出現(xiàn)文字被切的問題。5. 坐標(biāo)軸范圍設(shè)定的排查清單與個人習(xí)慣5.1 遇到坐標(biāo)異常時的排查清單坐標(biāo)范圍的問題通常不復(fù)雜但出錯時容易繞來繞去。我把排錯步驟整理成一張可以直接對照的表現(xiàn)象可能原因解決辦法坐標(biāo)軸范圍被拉得異常大數(shù)據(jù)里有離群值或錯誤值如1e6先range(x, na.rmTRUE)檢查數(shù)據(jù)再設(shè)定xlim/ylim指定xlim后圖形仍然超出base R默認(rèn)4%擴(kuò)展加上xaxsi, yaxsi用ggplot2畫日期圖xlim報錯limits類型與軸的scale類型不匹配用as.Date()或scale_x_date(limits...)xlim后統(tǒng)計線形狀改變xlim截斷了數(shù)據(jù)再統(tǒng)計需要“放大視圖”時改用coord_cartesian()直方圖柱子太少組距受完整數(shù)據(jù)影響與xlim無關(guān)手動指定breaks或先篩數(shù)據(jù)對數(shù)坐標(biāo)軸下圖形空白limits包含0或負(fù)數(shù)把limits下限改成大于0的值或加偏移量圖邊緣文字被裁掉坐標(biāo)范圍太窄、邊距不足調(diào)節(jié)par(mar)或theme(plot.margin)擴(kuò)大ylim排查時我的順序是先檢查數(shù)據(jù)本身有沒有NaN、Inf、異常大值再確認(rèn)坐標(biāo)范圍的數(shù)值類型是否匹配軸的刻度類型最后才懷疑繪圖函數(shù)的參數(shù)使用錯誤。超過一半的問題出在前兩步。5.2 我畫圖時固定的幾個操作習(xí)慣第一先畫默認(rèn)圖不改任何坐標(biāo)范圍。這一步的目的是看數(shù)據(jù)全貌包括有沒有離群值、分布形狀是什么樣的。很多人一上來就xlim、ylim手寫一通結(jié)果把一個重要異常值直接藏掉了自己還不知道。第二用range()和extendrange()隨時確認(rèn)邊界。寫代碼前先跑一下range(x, na.rm TRUE) extendrange(x, f 0.05)extendrange()可以按比例擴(kuò)展數(shù)據(jù)范圍得到比較合理的繪圖邊界。我通常拿它生成初版xlimplot(x, y, xlim extendrange(x, f 0.05), ylim extendrange(y, f 0.05))第三多圖對比必須統(tǒng)一坐標(biāo)范圍沒有例外。不管是用par(mfrow)、facet_wrap還是手動拼圖只要目的是對比不同組、不同時間段坐標(biāo)范圍不一致就等于數(shù)據(jù)造假。第四柱狀圖從0開始折線圖可以“斷軸”。柱子的高度代表數(shù)值大小不從0開始會讓柱子的視覺比例失真。折線圖和散點圖關(guān)注的是波動和趨勢可以裁剪掉沒有數(shù)據(jù)的空白區(qū)域讓細(xì)節(jié)更清楚。這個選擇不是技術(shù)問題而是表達(dá)問題。第五發(fā)表用的圖要控制主刻度數(shù)量。坐標(biāo)范圍一寬R會自作主張塞很多刻度線圖面很亂。我一般把主刻度控制在3到6個之間用axis()或scale_x_continuous(breaks)手動指定。5.3 坐標(biāo)范圍就是數(shù)據(jù)敘事的一部分最后分享一點個人體會。坐標(biāo)范圍從來不是單純的繪圖細(xì)節(jié)它決定讀者從同一份數(shù)據(jù)里看到什么“故事”。同一條上升曲線把橫軸拉長趨勢顯得平緩把縱軸壓窄波動顯得劇烈。自媒體時代有一句常用的話叫“圖表會說話”說到底圖表語言里最具操控性的就是坐標(biāo)范圍。我自己在檢查圖表時會刻意問三個問題這個xlim把哪些數(shù)據(jù)排除了這個ylim是真的需要這么窄還是為了讓差異看起來更大如果換一組尺度的數(shù)據(jù)這張圖的結(jié)論還成立嗎這三個問題問完能攔住不少誤導(dǎo)性的圖。坐標(biāo)范圍這個參數(shù)看起來只是兩個數(shù)字實際上它是圖上所有信息的邊界。搞清楚xlim和ylim的機(jī)制、分清截斷數(shù)據(jù)與縮放視圖的區(qū)別、養(yǎng)成固定坐標(biāo)對比的習(xí)慣你就掌握了數(shù)據(jù)可視化里最重要的一項基本功。畫圖這件事很多時候不是技術(shù)不夠而是對“邊界”的理解不到位。