現(xiàn)NC風(fēng)格分組小提琴圖:從數(shù)據(jù)到出圖)
做科研圖表復(fù)現(xiàn)這事我一直覺(jué)得最忌諱的就是只盯著別人的圖“照貓畫(huà)虎”不知道圖層背后為什么要這么疊。Nature Communications 里那類(lèi)分組小提琴圖乍看就是幾個(gè)“胖葫蘆”并排擺在一起可真要自己動(dòng)手從數(shù)據(jù)一步步畫(huà)出來(lái)你會(huì)發(fā)現(xiàn)里面牽扯到數(shù)據(jù)結(jié)構(gòu)、統(tǒng)計(jì)展示、期刊審美、甚至輸出分辨率一堆細(xì)節(jié)。這篇博文我就拿一個(gè)典型的 NC 圖表做靶子完整拆解怎么用 R ggplot2 復(fù)現(xiàn)一張可以投出去的分組小提琴圖——從讀圖思路到數(shù)據(jù)整理從基礎(chǔ)圖層到統(tǒng)計(jì)標(biāo)記再到我在實(shí)際操作里踩過(guò)的那些坑。這套東西更適合誰(shuí)看如果你是做生信、醫(yī)學(xué)統(tǒng)計(jì)、或者任何需要拿“分組對(duì)比”講故事的科研人員那這篇文章基本能幫你省掉一兩個(gè)星期的摸索時(shí)間。哪怕你現(xiàn)在只用過(guò) Excel 畫(huà)圖只要會(huì)一點(diǎn)點(diǎn) R 的基礎(chǔ)語(yǔ)法跟著思路走完一遍也能做出像模像樣的 NC 風(fēng)插圖。1. 這個(gè)圖要復(fù)現(xiàn)的到底是什么先讀懂圖表本身很多人拿到一張文獻(xiàn)里的圖第一反應(yīng)就是“我要把這個(gè)代碼搞到手”而不是先問(wèn)一句“這個(gè)圖在說(shuō)什么”。這是復(fù)現(xiàn)工作最大的誤區(qū)。你連圖里的信息層級(jí)都沒(méi)理清楚就算拿到原作者代碼換個(gè)數(shù)據(jù)一樣畫(huà)廢。1.1 小提琴圖 vs 箱線圖不只是“好看”先明確一個(gè)基本概念。箱線圖大家都熟它展示的是五數(shù)概括——最小值、下四分位數(shù)、中位數(shù)、上四分位數(shù)、最大值外加離群點(diǎn)。它的優(yōu)點(diǎn)是穩(wěn)健、簡(jiǎn)潔但代價(jià)是丟失了數(shù)據(jù)分布的“中間形態(tài)”。比如兩組數(shù)據(jù)一組是雙峰分布一組是均勻分布它們的箱線圖可能長(zhǎng)得幾乎一模一樣但真實(shí)的數(shù)據(jù)結(jié)構(gòu)完全不同。小提琴圖本質(zhì)上就是“箱線圖 核密度曲線”的合體把數(shù)據(jù)的概率密度曲線對(duì)稱地畫(huà)在兩側(cè)中間再疊一個(gè)精簡(jiǎn)的箱線圖。這樣你既能看出中位數(shù)和四分位區(qū)間又能直接看到數(shù)據(jù)在哪些區(qū)間聚集、哪里稀疏、是不是存在多峰。在 Nature Communications 這類(lèi)期刊里審稿人和讀者都更吃這一套因?yàn)樗麄儾还庀肟吹健坝袥](méi)有差異”還想看到“差異到底是怎么分布的”。分組小提琴圖就是在小提琴圖的基礎(chǔ)上把 x 軸再拆出一個(gè)分組維度。舉個(gè)例子你要比較兩種藥物處理下、四個(gè)時(shí)間點(diǎn)的某個(gè)細(xì)胞因子表達(dá)量。這時(shí)候 x 軸放時(shí)間點(diǎn)fill 映射藥物類(lèi)型每個(gè)時(shí)間點(diǎn)下就有兩把小提琴并排一眼看過(guò)去既能看到時(shí)間趨勢(shì)又能看到同一時(shí)間點(diǎn)上兩種藥物的差異。這比“一張圖里塞八個(gè)箱線圖”要清爽得多。提示判斷一張圖需不需要用分組小提琴圖有個(gè)簡(jiǎn)單標(biāo)準(zhǔn)——你是不是同時(shí)關(guān)心“兩個(gè)維度交叉后的分布形態(tài)”如果只關(guān)心均值差異箱線圖就夠了如果數(shù)據(jù)量大、分布復(fù)雜、還想展示樣本量那就上小提琴圖。1.2 分組小提琴圖到底在展示什么關(guān)系以 NC 論文里常見(jiàn)的一個(gè)場(chǎng)景為例縱軸是某個(gè)基因的表達(dá)量log2 歸一化后橫軸是“對(duì)照組 / 處理組 / 回復(fù)組”fill 顏色代表“患者是否響應(yīng)”。這個(gè)圖想回答的問(wèn)題有三個(gè)不同組別之間表達(dá)量的中位數(shù)有沒(méi)有顯著變化每一組內(nèi)部的樣本分布是集中還是離散是否存在離群亞群兩組之間的差異是整體平移還是僅僅尾部少數(shù)樣本在拉高均值這三個(gè)問(wèn)題普通箱線圖只能回答第一個(gè)散點(diǎn)圖能回答第二個(gè)但很難承載大樣本分組小提琴圖則是三個(gè)問(wèn)題一起答。所以你在復(fù)現(xiàn)之前一定要先搞清楚原圖作者用這個(gè)圖想論證什么那個(gè)核心信息是什么后面你怎么配色、怎么加統(tǒng)計(jì)標(biāo)記、怎么排布子圖都是圍繞這個(gè)核心信息服務(wù)的。另外一個(gè)很容易忽略的點(diǎn)是樣本量。小提琴圖的缺點(diǎn)是樣本量少的時(shí)候核密度估計(jì)會(huì)非常不穩(wěn)定畫(huà)出來(lái)跟一根根火柴棍似的很難看也容易誤導(dǎo)讀者。這也是為什么實(shí)際復(fù)現(xiàn)時(shí)絕大多數(shù)人會(huì)在小提琴圖里疊加散點(diǎn)也就是所謂的“蜂群圖”或“抖動(dòng)散點(diǎn)”每個(gè)點(diǎn)代表一個(gè)真實(shí)樣本——既保留原始數(shù)據(jù)透明度又彌補(bǔ)小提琴圖在少量樣本下的失真問(wèn)題。這在 NC 里幾乎是標(biāo)配。2. 數(shù)據(jù)準(zhǔn)備與工具選型復(fù)現(xiàn)之前先把地基打牢我見(jiàn)過(guò)太多人代碼跑不通最后發(fā)現(xiàn)不是函數(shù)用錯(cuò)了而是數(shù)據(jù)結(jié)構(gòu)從一開(kāi)始就是亂的。繪圖代碼只是最后一公里前面九十九公里的數(shù)據(jù)整理如果沒(méi)做好后面全是連鎖反應(yīng)。2.1 數(shù)據(jù)結(jié)構(gòu)長(zhǎng)格式 vs 寬格式ggplot2 有一個(gè)鐵律它只能吃“長(zhǎng)格式數(shù)據(jù)”long data。所謂長(zhǎng)格式就是每一行是一個(gè)觀測(cè)樣本每一列是一個(gè)變量。以剛才那個(gè)“基因表達(dá)量”的例子來(lái)說(shuō)數(shù)據(jù)框應(yīng)該是這樣的sample_idgroupresponseexpressionS01controlR6.23S02controlNR5.11S03treatmentR8.02............每一行代表一個(gè)患者的某個(gè)樣本group列說(shuō)明它屬于哪個(gè)實(shí)驗(yàn)分組response列說(shuō)明它是不是響應(yīng)者expression列是我們要畫(huà)到 y 軸上的數(shù)值。很多新手拿到手的數(shù)據(jù)是寬格式的比如不同列的列名是“control_R”“control_NR”“treatment_R”這種。這種格式給 ggplot2 用輕則報(bào)錯(cuò)重則畫(huà)出完全錯(cuò)誤的圖。所以復(fù)現(xiàn)的第一步永遠(yuǎn)是檢查你的數(shù)據(jù)是不是長(zhǎng)格式。不是的話用tidyr::pivot_longer()把它轉(zhuǎn)過(guò)來(lái)。library(tidyr) # 假設(shè)寬格式數(shù)據(jù)長(zhǎng)這樣 # sample_id, control_R, control_NR, treatment_R, treatment_NR df_long - df_wide %% pivot_longer( cols starts_with(control):starts_with(treatment), names_to c(group, response), names_sep _, values_to expression )這里names_sep _是個(gè)容易出錯(cuò)的地方如果列名里有多余下劃線或者大寫(xiě)小寫(xiě)不統(tǒng)一拆分就會(huì)出問(wèn)題。我的習(xí)慣是在整理原始數(shù)據(jù)時(shí)列名只用小寫(xiě)字母和下劃線且下劃線只出現(xiàn)一次否則后面所有處理都是在給自己埋雷。2.2 工具選擇R ggplot2 還是 Python seaborn復(fù)現(xiàn) NC 圖表我強(qiáng)烈建議首選 R ggplot2。原因有三點(diǎn)一是 ggplot2 的圖層語(yǔ)法邏輯和期刊圖表的信息層級(jí)天然匹配你想在圖上疊箱線圖、疊散點(diǎn)、加統(tǒng)計(jì)線都是“往畫(huà)布上堆圖層”的操作非常直觀二是 R 生態(tài)里統(tǒng)計(jì)檢驗(yàn)的工具太全了ggpubr、rstatix這些包直接幫你把顯著性標(biāo)記畫(huà)上去省掉手動(dòng)計(jì)算 p 值再手動(dòng)標(biāo)注的麻煩三是 Nature 系期刊的圖表風(fēng)格半開(kāi)主題、極簡(jiǎn)網(wǎng)格、克制的配色基本就是 ggplot2 主題定制出來(lái)的你很難在 Python 里找到這么順手的“一鍵風(fēng)格”。不是說(shuō) Python 不行。如果你的整個(gè)分析流程都在 Python 里用 seaborn 畫(huà)sns.violinplot也完全能復(fù)現(xiàn)大致效果特別是splitTrue這個(gè)參數(shù)可以畫(huà)出漂亮的“半分小提琴圖”。但如果你要精細(xì)控制每個(gè)圖層最后還要拼多圖、調(diào)主題R 的靈活度更勝一籌。我自己是兩套工具都用但最終出圖基本都落到 R 上。標(biāo)題里既然點(diǎn)名了 Nature Communications我就默認(rèn)你要走“期刊級(jí)復(fù)現(xiàn)”這條路那工具選定為 R ggplot2 是性價(jià)比最高的選擇。版本方面用 R 4.2 以上ggplot2 3.4 以上兩個(gè)包就能覆蓋大部分需求。部分進(jìn)階功能需要ggpubr、patchwork、ggsignif后面會(huì)具體提到。3. 用 ggplot2 一步一步畫(huà)出分組小提琴圖3.1 基礎(chǔ)圖層搭建假設(shè)數(shù)據(jù)已經(jīng)整理成了長(zhǎng)格式df_long我們要畫(huà)的圖是x 軸 處理分組fill 是否響應(yīng)y 軸 表達(dá)量。先寫(xiě)出最基礎(chǔ)的代碼library(ggplot2) p_base - ggplot(df_long, aes(x group, y expression, fill response)) geom_violin( position position_dodge(0.8), width 0.6, alpha 0.7, scale width ) theme_classic(base_size 14)這里有兩個(gè)參數(shù)值得單獨(dú)拿出來(lái)說(shuō)。第一個(gè)是position_dodge(0.8)。它控制同一 x 位置下兩組小提琴之間左右錯(cuò)開(kāi)的幅度。0.8 的意思是小提琴的總寬度占 x 軸單位寬度的 80%彼此之間留一點(diǎn)縫。這個(gè)值太小兩組會(huì)疊在一起太大兩個(gè)小提琴離得太遠(yuǎn)視覺(jué)上像兩棵孤立的樹(shù)。NC 的圖里常見(jiàn)的 dodge 值在 0.7~0.9 之間我一般取 0.8。第二個(gè)是scale width。這是個(gè)特別關(guān)鍵但特別容易被忽略的參數(shù)。默認(rèn)情況下ggplot2 中不同組的小提琴寬度是根據(jù)該組樣本量來(lái)的樣本量大的組小提琴更寬。這個(gè)默認(rèn)行為在工作時(shí)往往不是我們想要的——你想比較的是“分布形態(tài)”而不是“樣本量差異”在視覺(jué)上的喧賓奪主。設(shè)成scale width之后所有組的小提琴寬度統(tǒng)一極大值、極小值的組不會(huì)出現(xiàn)“一個(gè)大葫蘆帶一個(gè)小葫蘆”的失衡感。這是我建議所有人復(fù)現(xiàn) NC 圖時(shí)第一件要做的事。alpha 0.7是給小提琴加一點(diǎn)透明度。因?yàn)樵诤罄m(xù)步驟里我們會(huì)在里面疊箱線圖和散點(diǎn)太實(shí)的填充色會(huì)擋住后面的內(nèi)容。透明度這一點(diǎn)文獻(xiàn)里通常直接用實(shí)色但實(shí)際出圖時(shí)半透明反而更高級(jí)也方便讀者看到重疊在內(nèi)部的點(diǎn)。3.2 疊加箱線圖與散點(diǎn)信息密度的藝術(shù)基礎(chǔ)小提琴畫(huà)完之后圖還“空”。NC 的圖通常不會(huì)只放一把光禿禿的小提琴它會(huì)夾一層迷你箱線圖再撒上原始樣本點(diǎn)。箱線圖直接用geom_boxplot疊上去但有一點(diǎn)必須注意它的position_dodge寬度必須和小提琴的完全一致否則箱線圖會(huì)跑到小提琴外面去。p_box - p_base geom_boxplot( width 0.1, position position_dodge(0.8), outlier.shape NA, coef 0, alpha 0.8 )這里width 0.1控制箱線圖“腰身”的粗細(xì)太寬會(huì)蓋住小提琴的密度輪廓太窄又看不清箱體。coef 0的意思是不要畫(huà)離群點(diǎn)因?yàn)槲覀円呀?jīng)要疊散點(diǎn)了離群點(diǎn)交給散點(diǎn)去展示箱線圖只管五數(shù)概括就行。散點(diǎn)我用geom_jitter或者geom_point 手動(dòng)抖動(dòng)。注意散點(diǎn)的 dodge 寬度同樣要和小提琴保持一致否則所有點(diǎn)都會(huì)擠在同一條垂線上完全看不出分布p_dot - p_box geom_jitter( position position_jitterdodge( jitter.width 0.15, dodge.width 0.8 ), size 1.2, alpha 0.6 )N 大的時(shí)候比如每組上百個(gè)樣本直接疊散點(diǎn)會(huì)讓圖變成一坨墨跡。這時(shí)候有兩種處理一是把點(diǎn)的透明度調(diào)到 0.3 以下讓密集區(qū)域自然變深二是改用ggbeeswarm包里的geom_beeswarm讓點(diǎn)像蜂群一樣自動(dòng)排布不重疊。NC 論文中那種每個(gè)點(diǎn)清晰可辨的效果大多是用 beeswarm 或者geom_quasirandom做出來(lái)的比純 jitter 好看得多。3.3 分面分組維度再多一層時(shí)怎么辦有些時(shí)候分組變量不止兩個(gè)。比如研究里有“性別”這一層混雜因素你想分別看男性和女性里的處理效應(yīng)。這時(shí)候硬塞進(jìn)同一個(gè)坐標(biāo)系會(huì)非常擁擠正確的做法是用facet_wrap分面。p_wrap - p_dot facet_wrap(~ sex, nrow 1)在 NC 圖中你經(jīng)常能看到一行兩三個(gè)小圖并排的排版每個(gè)小圖就是同一個(gè)圖在不同亞組下的切片。這種方式的好處是既保證了圖形元素的一致性y 軸范圍相同便于橫向?qū)Ρ扔直苊饬诵畔⑦^(guò)載。用facet_wrap之后還有個(gè)細(xì)節(jié)默認(rèn)情況下每個(gè)面都會(huì)帶上自己的坐標(biāo)軸刻度這在并排對(duì)比時(shí)其實(shí)沒(méi)太大必要還占空間。我一般會(huì)加一句scales fixed默認(rèn)就是 fixed同時(shí)把 x 軸標(biāo)簽只保留在每個(gè)面板的最下方p_wrap - p_wrap theme( strip.background element_blank(), strip.text element_text(size 13, face bold) )這樣上下留白少了圖更緊湊也更符合期刊那種干干凈凈的氣質(zhì)。3.4 配色與主題定制向 Nature 排版靠攏Nature Communications 的圖風(fēng)格說(shuō)難聽(tīng)點(diǎn)叫“性冷淡”說(shuō)好聽(tīng)點(diǎn)叫“克制”低飽和度的配色、無(wú)背景網(wǎng)格線、字體大小統(tǒng)一、標(biāo)注精煉。復(fù)現(xiàn)的時(shí)候這幾點(diǎn)一個(gè)都不能落下。配色方面我建議別用默認(rèn)的 ggplot2 調(diào)色板那個(gè)偏花哨改用比較穩(wěn)的配色方案。比如library(RColorBrewer) my_colors - c(#E64B35, #4DBBD5, #00A087, #3C5488)前兩個(gè)是常用的紅藍(lán)對(duì)比適合“處理 vs 對(duì)照”“響應(yīng) vs 不響應(yīng)”這類(lèi)二分組。如果你要做多分組對(duì)比也可以直接用scale_fill_manual(values my_colors)手動(dòng)指定。這里有一個(gè)經(jīng)驗(yàn)雙色對(duì)比盡量選“色相差異大、飽和度不高”的顏色紅配藍(lán)是最穩(wěn)妥的紅配綠在色盲讀者眼里可能會(huì)翻車(chē)。p_final - p_wrap scale_fill_manual(values my_colors[1:2], labels c(Non-responder, Responder)) labs( x Treatment group, y Relative expression (log2), fill Response ) theme_classic(base_size 14) theme( legend.position top, legend.key.size unit(0.6, cm), axis.line element_line(color black, linewidth 0.5), axis.ticks element_line(color black, linewidth 0.5) )這里theme_classic是天然適合 NC 風(fēng)的基礎(chǔ)主題沒(méi)有網(wǎng)格線只有兩條坐標(biāo)軸。我在此基礎(chǔ)上加了黑色的軸線避免默認(rèn)的灰色在打印后發(fā)虛。注意配色盡量不要用那些“熒光色”印刷出來(lái)會(huì)偏色偏得厲害。編輯和審稿人對(duì)顏色的偏好往往是“能區(qū)分、不刺眼”。你可以在一個(gè)陽(yáng)光充足的屏幕上把圖縮到 80% 再放大回 100%看看區(qū)分度還夠不夠明顯——這是我檢驗(yàn)配色的土辦法實(shí)測(cè)很有效。4. 進(jìn)階統(tǒng)計(jì)檢驗(yàn)、分面小圖與期刊出圖規(guī)范一張圖畫(huà)得再好看沒(méi)有統(tǒng)計(jì)檢驗(yàn)的支撐在 NC 這種期刊面前就等于“裸奔”。復(fù)現(xiàn)文獻(xiàn)圖表統(tǒng)計(jì)標(biāo)記這塊幾乎是必選項(xiàng)。4.1 添加顯著性標(biāo)記最常見(jiàn)的需求是比較“處理組 vs 對(duì)照組”在特定分組下的差異然后把 p 值用星號(hào)或者數(shù)字標(biāo)到圖上。做法通常是用ggpubr::stat_compare_means也可以用ggsignif::geom_signif自己手動(dòng)指定比較組。library(ggpubr) p_stat - p_final stat_compare_means( method wilcox.test, comparisons list(c(control, treatment)), label p.signif, label.y.npc 0.95 )這里的方法要謹(jǐn)慎選擇如果你的數(shù)據(jù)大致正態(tài)且各組方差齊性可以用 t 檢驗(yàn)如果像絕大多數(shù)組學(xué)數(shù)據(jù)那樣偏態(tài)分布用 wilcoxon 秩和檢驗(yàn)更穩(wěn)。NC 的圖里常見(jiàn)標(biāo)注形式有p 0.003、p 0.001、或者直接用星號(hào)體系* p 0.05** p 0.01*** p 0.001。如果你的圖里同時(shí)有好幾組比較我建議用星號(hào)因?yàn)閿?shù)字多了排版會(huì)亂而且編輯更喜歡“簡(jiǎn)潔”。如果你是按照response分組還想在每個(gè)group內(nèi)比較 responder 和 non-responder 的差異那就需要換一種寫(xiě)法p_stat - p_final stat_compare_means( method wilcox.test, aes(label ..p.signif..), comparisons list(c(R, NR)), label.y.npc 0.9 )stat_compare_means默認(rèn)會(huì)在每個(gè)面板即每個(gè)group里獨(dú)立做分組檢驗(yàn)非常省事。但要注意label.y.npc這個(gè)參數(shù)它是基于“面板內(nèi) y 軸范圍比例”定位標(biāo)簽的設(shè)成 0.95 會(huì)放在靠近頂部的位置。如果畫(huà)完發(fā)現(xiàn)標(biāo)簽跟數(shù)據(jù)點(diǎn)疊在一起微調(diào)這個(gè)數(shù)值就行。4.2 樣本量標(biāo)注與誤差線期刊審稿人特別看重“你的結(jié)論是不是建在小樣本上”。所以很多 NC 圖會(huì)在 x 軸刻度標(biāo)簽下面或者小提琴底部標(biāo)明每組樣本量 n。做法很簡(jiǎn)單就是拼一個(gè)文本層n_label - df_long %% group_by(group, response) %% summarise(n n(), .groups drop) n_label$y - min(df_long$expression) - (max(df_long$expression) - min(df_long$expression)) * 0.08然后把這個(gè)n_label映射上去。不過(guò)更干凈的做法是直接改 x 軸的標(biāo)簽把“Control (n20)”直接寫(xiě)進(jìn)刻度標(biāo)簽里new_labels - c( Control (n20), Treatment (n22) ) p_final - p_final scale_x_discrete(labels new_labels)這個(gè)方案的缺點(diǎn)是如果同一個(gè) x 分組下還有response兩個(gè)子組那這里的 “n” 就會(huì)是合計(jì)樣本量容易誤導(dǎo)。所以我實(shí)際出圖時(shí)更傾向于把 n 標(biāo)注成子圖內(nèi)的文字注釋或者放在圖例里。圖例里面的寫(xiě)法是p_final scale_fill_manual( values my_colors, labels c(Non-responder (n12), Responder (n16)) )這種做法干凈、直接而且不擠占繪圖區(qū)域是我最推薦的方案。4.3 輸出矢量圖與分辨率設(shè)置你辛辛苦苦畫(huà)好的圖如果在最后導(dǎo)出的時(shí)候用了個(gè) 72 dpi 的 JPG 發(fā)給了導(dǎo)師那前面所有工作都白費(fèi)了。期刊投稿對(duì)圖片的最低要求通常是 300 dpi但最佳實(shí)踐永遠(yuǎn)是存矢量圖PDF 或 SVG。ggsave( Figure_1_violin.pdf, plot p_final, width 7, height 5, units in, dpi 300, device pdf )這里device pdf會(huì)保證里面的文字、線條全部變成矢量元素放大多少倍都不糊。如果你投稿系統(tǒng)要求 TIFF那就得先輸出一個(gè)高分辨率位圖ggsave( Figure_1_violin.tiff, plot p_final, width 7, height 5, units in, dpi 600, compression lzw )TIFF 用 600 dpi 是個(gè)穩(wěn)妥的保險(xiǎn)值因?yàn)橛械钠诳谟∷r(shí)會(huì)按 600 dpi 重新采樣純 300 dpi 的圖印出來(lái)有時(shí)候會(huì)邊緣發(fā)虛特別是那些細(xì)線條。用compression lzw是為了做無(wú)損壓縮文件體積小很多但畫(huà)質(zhì)不受影響。字體方面如果你用的是中文系統(tǒng)且 R 里沒(méi)有配置好字體導(dǎo)出 PDF 時(shí)容易出現(xiàn)字體缺失或亂碼。穩(wěn)妥做法是出圖時(shí)用系統(tǒng)自帶的英文默認(rèn)字體R 默認(rèn)通常能處理中文字體一定要在投稿前確認(rèn)嵌入了。我的習(xí)慣是終稿一律把字體切成Arial或Helvetica這類(lèi)無(wú)襯線字體統(tǒng)一又干凈p_final - p_final theme(text element_text(family Arial))5. 復(fù)盤(pán)與避坑復(fù)現(xiàn)過(guò)程中最常見(jiàn)的幾個(gè)問(wèn)題下面這部分是純經(jīng)驗(yàn)總結(jié)。我當(dāng)年第一次復(fù)現(xiàn) NC 的小提琴圖前前后后折騰了一個(gè)多星期現(xiàn)在回頭看至少有一半的時(shí)間都花在了一些文檔里根本不會(huì)寫(xiě)的奇怪 bug 上。5.1 常見(jiàn)問(wèn)題速查表現(xiàn)象原因解決方法箱線圖漂在小提琴外側(cè)geom_boxplot的 dodge 寬度和小提琴不一致確保兩者的position_dodge寬度完全一致小提琴圖一片糊看不出密度形態(tài)樣本量太大核密度帶寬選擇不當(dāng)調(diào)adjust 0.5或bw SJ或改用geom_quasirandom展示點(diǎn)兩組小提琴寬度懸殊默認(rèn)scale area寬度由樣本量決定改成scale width散點(diǎn)全部堆在一條線上geom_jitter的抖動(dòng)寬度太小jitter.width調(diào)到 0.15~0.3且dodge.width必須匹配p 值標(biāo)簽重疊、位置飄出畫(huà)面label.y.npc設(shè)定不合理縮小數(shù)值或改用label.y直接指定絕對(duì)坐標(biāo)導(dǎo)出的 PDF 打開(kāi)后中文字體亂碼字體未嵌入換用 Arial 或嵌入字體后再導(dǎo)出同一個(gè) x 分組下的兩組小提琴間距太窄position_dodge(0.8)小于實(shí)際需要的值調(diào)大 dodge或者把 x 軸映射改成interaction(group, response)分面后各子圖坐標(biāo)范圍不一致對(duì)比時(shí)誤讀需要獨(dú)立尺度或者固定尺度按需設(shè)scales free但注意對(duì)比公平性這個(gè)表不是給出來(lái)好看的是每一行都是我或周?chē)娜苏鎸?shí)踩過(guò)的。尤其那個(gè)“箱線圖漂移”的問(wèn)題新手幾乎必然遇到。其實(shí)原理很簡(jiǎn)單ggplot2 的position_dodge并不會(huì)自動(dòng)同步多個(gè)圖層之間的躲避寬度你不顯式設(shè)置箱線圖就會(huì)按自己的默認(rèn)寬度0.75躲和小提琴不一致自然錯(cuò)位。所以寫(xiě)代碼的時(shí)候養(yǎng)成把所有圖層的position_dodge參數(shù)都顯式寫(xiě)出來(lái)的習(xí)慣能救你命。5.2 一些值得砸進(jìn)腦子里的心得第一數(shù)據(jù)容錯(cuò)率比代碼容錯(cuò)率重要得多。我復(fù)現(xiàn)時(shí)吃過(guò)最大的虧不是畫(huà)圖而是整理數(shù)據(jù)的時(shí)候把某個(gè)樣本的組別標(biāo)錯(cuò)了一個(gè)字母結(jié)果小提琴圖里多出了一個(gè)“幽靈群”。后來(lái)我養(yǎng)成了一個(gè)習(xí)慣畫(huà)圖之前先跑一行table(df_long$group, df_long$response)看看交叉頻數(shù)和預(yù)期是否一致。這行代碼 1 秒鐘就能跑完能擋住 90% 的數(shù)據(jù)錯(cuò)漏。第二不要過(guò)度美化。有的朋友畫(huà)完圖覺(jué)得太平淡硬要加漸變填充、加陰影、加 3D 效果。NC 的風(fēng)格從來(lái)都不是炫技而是“信息盡可能清晰”。你加了再多裝飾審稿人看的還是數(shù)據(jù)本身。我記得我導(dǎo)師說(shuō)的一句話“圖表是數(shù)據(jù)的翻譯官不是數(shù)據(jù)的化妝師。”這話糙理不糙現(xiàn)在也是我審稿時(shí)的第一觀感來(lái)源。第三圖例位置很有講究。NC 的圖例通常放在頂部或右側(cè)我自己的偏好是頂部居中。legend.position top配合legend.justification center就可以實(shí)現(xiàn)。圖例放頂部有個(gè)好處橫向分面的時(shí)候每個(gè)小圖的寬度可以更大數(shù)據(jù)的主體展示區(qū)不會(huì)被圖例擠壓。第四如果你要畫(huà)的是那種“半分小提琴”即一組數(shù)據(jù)只畫(huà)半邊而另一半留給對(duì)照組可以用geom_split_violin的現(xiàn)成實(shí)現(xiàn)比如see包里的geom_violin配合split TRUE。這種圖在 sample 對(duì)照設(shè)計(jì)里特別常見(jiàn)但注意它的數(shù)據(jù)格式要求更嚴(yán)格x 軸必須是兩兩配對(duì)的分組變量。復(fù)現(xiàn)前先確認(rèn)你的數(shù)據(jù)是不是這種結(jié)構(gòu)。最后再說(shuō)一個(gè)使用patchwork拼圖的小技巧。NC 的正式圖表往往是 A、B、C 三個(gè)子圖拼成一張 Figure小提琴圖可能只是其中的 Panel A。我會(huì)用patchwork包做拼接library(patchwork) combined - p_stat p_heatmap p_curve plot_annotation(tag_levels A) theme(plot.tag element_text(size 18, face bold))tag_levels A就是自動(dòng)生成左上角的大寫(xiě)字母標(biāo)記。這個(gè)包在最開(kāi)始用的時(shí)候容易犯一個(gè)錯(cuò)忘記在所有子圖后面加來(lái)統(tǒng)一主題導(dǎo)致三個(gè)圖的字體大小不一致拼起來(lái)非常難看。統(tǒng)一主題的寫(xiě)法就是上面這種把所有theme統(tǒng)一到后面拼出來(lái)的圖才有一體感。我自己在實(shí)際操作里還有一個(gè)執(zhí)念每次把拼好的圖導(dǎo)出前都會(huì)用眼睛在“縮小到 50%”和“放大到 200%”兩個(gè)縮放級(jí)別各看一遍??s小看整體布局是否失衡放大看細(xì)節(jié)線條是否發(fā)虛、點(diǎn)是否糊成一團(tuán)。這一步看起來(lái)很笨但能提前發(fā)現(xiàn)很多讓編輯皺眉的問(wèn)題。做完這些檢查一張能拿去投稿的分組小提琴圖才算真正落地。后面你再?gòu)?fù)現(xiàn)別的圖其實(shí)都是同一個(gè)套路——先把圖讀明白再把數(shù)據(jù)捋順然后一層圖層地往上搭最后死磕輸出細(xì)節(jié)。這條路走一遍后面就通了。