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