ggplot2 기초 — 바이오 데이터 그래프 그리기 (산점도·막대·박스플롯, R 실전)

TL;DR — ggplot2 (그래픽 문법, grammar of graphics)는
ggplot(data, aes(x, y)) + geom_*()처럼 레이어를 +로 쌓아 그림을 그립니다. 데이터·미적매핑(aesthetic mapping)·기하 도형(geom)을 분리해서 생각하는 게 전부예요.이 글은 발현·그룹 더미 데이터로 산점도·막대(평균±오차)·박스플롯을 그리고, 색·소분할(facet)·테마·저장까지 복붙하면 그대로 도는 코드로 따라갑니다. 이 블로그의 Volcano plot·히트맵·PCA 글이 쓰는 문법의 바닥을 채우는 글입니다. (코드는 ggplot2 4.0 / 3.5+ 기준으로 실제 실행해 확인했습니다.)
🔗 관련 글 · 이 문법을 응용한 바이오 그래프: Volcano plot 시각화(ggplot2 응용) · ComplexHeatmap 히트맵 · PCA로 샘플 QC
이 글에서 그릴 것
최종 결과물은 산점도 + 막대그래프 + 박스플롯, 그리고 유전자별로 쪼갠 facet 패널입니다. 발현값(log2 발현)과 그룹(대조군·처리군) 더미 데이터를 코드로 만들어, ggplot2 문법을 한 겹씩 쌓아 보겠습니다. 핵심은 단 한 줄로 요약됩니다 — 데이터를 넣고(ggplot), 무엇을 어디에 그릴지 매핑하고(aes), 어떤 도형으로 그릴지 고른다(geom_*). 나머지는 전부 이 위에 +로 얹는 장식입니다.
# ggplot2의 한 문장 — 이 패턴만 외우면 절반은 끝
ggplot(데이터, aes(x = 변수1, y = 변수2)) + # 데이터 + 미적매핑
geom_point() # 기하 도형(여기선 점)

1. 사전 준비 — 설치와 예시 데이터
ggplot2는 단독 패키지로 써도 되고, 데이터 정리용 패키지 묶음인 tidyverse의 일부로 설치해도 됩니다. tidyverse를 깔면 ggplot2 (그래픽 문법)·dplyr (데이터 가공)·tidyr 등이 한 번에 들어와 편합니다.
install.packages("tidyverse") # ggplot2 + dplyr + tidyr ... 한 번에 (최초 1회)
# 또는 그래프만 필요하면:
# install.packages("ggplot2")
library(ggplot2) # 그래프 문법
예시는 바이오에서 가장 흔한 형태 — 샘플마다 유전자 발현값이 있고, 대조군·처리군 그룹이 붙은 긴 형식(long format) 데이터프레임으로 만듭니다. set.seed()로 난수를 고정했으니 여러분 손에서도 똑같은 숫자가 나옵니다.
set.seed(42)
genes <- c("BRCA1", "TP53", "EGFR", "MYC")
expr <- data.frame(
sample = rep(paste0("S", 1:12), each = 4), # 샘플 12개
group = rep(rep(c("Control", "Treated"), each = 4), times = 3), # 대조/처리
gene = rep(genes, times = 12), # 유전자 4종
log2expr = round(rnorm(48, mean = 8, sd = 1.5), 2), # log2 발현값
depth = round(runif(48, 5, 30), 1) # 시퀀싱 깊이(가짜)
)
expr$group <- factor(expr$group, levels = c("Control", "Treated")) # 대조군을 기준으로
# 처리군에서 EGFR만 발현을 올려, '효과'를 일부러 심는다
expr$log2expr[expr$gene == "EGFR" & expr$group == "Treated"] <-
expr$log2expr[expr$gene == "EGFR" & expr$group == "Treated"] + 2.5
head(expr, 4)
sample group gene log2expr depth
1 S1 Control BRCA1 10.06 13.3
2 S1 Control TP53 7.15 17.9
3 S1 Control EGFR 8.54 23.6
4 S1 Control MYC 8.95 20.5
무엇이 보이나 — 한 행이 한 관측(샘플 × 유전자)입니다. 열은 sample·group·gene·log2expr·depth. ggplot2는 이렇게 한 행 = 한 점인 긴 형식을 가장 좋아합니다. 엑셀처럼 유전자가 열마다 흩어진 넓은 형식(wide)이면 tidyr::pivot_longer()로 먼저 녹여야 합니다.
2. 첫 그래프 — ggplot() + aes() + geom_point()
가장 단순한 산점도부터. 깊이(depth)와 발현(log2expr)의 관계를 점으로 찍습니다. 세 조각만 기억하세요 — ggplot(데이터), aes(x, y), geom_point().
ggplot(expr, aes(x = depth, y = log2expr)) + # 데이터 + x·y 매핑
geom_point() # 점으로 그린다
무엇이 보이나 — 빈 좌표계 위에 점 48개가 찍힙니다. aes() 안에 적은 변수는 데이터 열 이름이고, ggplot2가 알아서 축 범위·눈금·라벨을 잡아 줍니다. +로 다음 줄에 geom_*을 얹는 게 핵심이에요. +는 반드시 줄 끝에 두세요 — 줄 맨 앞에 두면 R은 앞 줄에서 문장이 끝난 줄 알고 에러를 냅니다.
3. 색·모양 매핑 — aes 안 vs 밖
이제 그룹을 색으로 구분해 봅시다. 그룹별로 색을 다르게 하려면 color를 aes() 안에 넣습니다. 데이터 열에 따라 변하는 건 전부 aes() 안, 모든 점에 똑같이 적용할 고정값은 aes() 밖입니다.
ggplot(expr, aes(x = depth, y = log2expr,
color = group, shape = group)) + # 그룹 → 색·모양 (매핑)
geom_point(size = 3, alpha = 0.8) # 크기·투명도는 고정값 (밖)
무엇이 보이나 — 대조군·처리군이 다른 색·다른 모양으로 갈리고, 오른쪽에 범례가 자동으로 생깁니다. 반면 size = 3·alpha = 0.8은 aes() 밖에 있으니 모든 점에 똑같이 적용되고 범례에 안 뜹니다. 이 안/밖 구분이 ggplot2 초보가 가장 많이 헷갈리는 지점입니다.
aes() 안, 항상 같으면 aes() 밖." 예컨대 geom_point(color = "steelblue")는 모든 점을 파랗게(고정), geom_point(aes(color = group))는 그룹별로 색을 나눕니다(매핑).4. 막대그래프 — 평균±오차 (geom_col / stat_summary)
막대는 두 갈래입니다. 값을 그대로 막대 높이로 쓰면 geom_col(), 행 개수를 세서 빈도 막대를 그리면 geom_bar()(기본 stat = "count"). 바이오에서 자주 쓰는 그룹 평균 ± 표준오차 막대는 stat_summary() 한 줄로 깔끔하게 나옵니다 — 평균을 따로 계산할 필요가 없어요.
ggplot(expr, aes(x = gene, y = log2expr, fill = group)) +
stat_summary(fun = mean, geom = "col", # 그룹 평균을 막대로
position = position_dodge(0.8), width = 0.7) +
stat_summary(fun.data = mean_se, geom = "errorbar", # 평균±표준오차 막대
position = position_dodge(0.8), width = 0.2) +
labs(x = NULL, y = "평균 log2 발현", fill = "그룹")
무엇이 보이나 — 유전자 4종 × 그룹 2개의 평균 막대가 나란히(position_dodge) 서고, 위에 오차막대(errorbar)가 얹힙니다. EGFR만 처리군에서 막대가 확 올라간 게 보이죠 — 우리가 일부러 심은 효과입니다. fill은 막대·박스처럼 면을 칠하는 미적매핑이고, color는 선·점의 색입니다. 둘을 혼동하면 막대 테두리만 칠해지니 주의하세요.
5. 박스플롯 — 분포를 한눈에 (geom_boxplot)
막대는 평균만 보여 주지만, 박스플롯(box plot)은 중앙값·사분위수·이상치까지 분포를 통째로 보여 줍니다. 발현 데이터엔 박스플롯이 더 정직한 선택일 때가 많아요. 점을 흩뿌리는 geom_jitter()를 겹치면 실제 데이터 개수까지 드러납니다.
ggplot(expr, aes(x = group, y = log2expr, fill = group)) +
geom_boxplot(width = 0.6, alpha = 0.8) +
geom_jitter(width = 0.12, size = 1.6, alpha = 0.5) # 점을 살짝 흔들어 겹침 방지
무엇이 보이나 — 그룹별 박스(상자·수염)와 그 위에 흩뿌린 점들이 함께 보입니다. 레이어는 쓴 순서대로 위에 쌓이므로, geom_boxplot을 먼저, geom_jitter를 나중에 둬야 점이 박스 위에 올라옵니다. 순서를 바꾸면 박스가 점을 덮어 버립니다.

6. 소분할(facet) — 유전자별로 쪼개기
지금 박스플롯은 유전자를 뭉뚱그렸습니다. 유전자마다 따로 보고 싶을 때 facet_wrap()이 데이터를 자동으로 쪼개 작은 패널 여러 개로 깝니다 — 코드 한 줄로요.
ggplot(expr, aes(x = group, y = log2expr, fill = group)) +
geom_boxplot(width = 0.6) +
facet_wrap(~ gene, nrow = 1) # gene 값마다 패널 하나씩, 한 줄로
무엇이 보이나 — BRCA1·TP53·EGFR·MYC 네 패널이 한 줄로 나란히 뜨고, EGFR 패널에서만 처리군 박스가 위로 올라간 게 또렷합니다. 변수 둘로 격자를 짜려면 facet_grid(행변수 ~ 열변수)를 씁니다. facet_wrap(~ gene)처럼 물결표(~) 뒤에 쪼갤 변수를 적는다고 기억하세요.
7. 색·축·라벨·테마 — 외형 다듬기
마지막으로 발표용으로 다듬습니다. 색은 scale_fill_manual()로 직접 지정, 제목·축 라벨은 labs(), 전체 외형은 theme_*() + 세부 theme()로 손봅니다. 이 레이어들은 그림의 내용이 아니라 표현을 바꿉니다.
ggplot(expr, aes(x = group, y = log2expr, fill = group)) +
geom_boxplot(width = 0.6) +
facet_wrap(~ gene, nrow = 1) +
scale_fill_manual(values = c(Control = "#6BAB97", # 색 직접 지정
Treated = "#E76F51")) +
labs(title = "유전자별 발현 비교", # 제목·축·범례 라벨
x = NULL, y = expression(log[2]~expression),
fill = "그룹") +
theme_bw(base_size = 13) + # 흑백 테마, 글자 크기
theme(legend.position = "top", # 범례를 위로
strip.background = element_rect(fill = "grey95")) # facet 라벨 배경
무엇이 보이나 — 손맛 나는 색, 위로 올라간 범례, 깔끔한 흰 배경의 발표용 그림이 됩니다. theme_bw()·theme_minimal()·theme_classic() 같은 완성형 테마를 먼저 고른 뒤, 세부는 theme()로 덮어쓰는 게 정석입니다. 축을 로그로 바꾸려면 scale_y_log10()을, 시간에 따른 추세는 geom_line(linewidth = 1)을 얹으면 됩니다. (ggplot2 3.4부터 선 굵기는 size가 아니라 linewidth입니다.)
scale_y_log10()(데이터 변환), 단순히 보이는 범위만 자르려면 coord_cartesian(ylim = ...)(데이터는 그대로, 화면만 확대)를 씁니다. ylim()으로 자르면 범위 밖 점이 계산에서 빠져 박스플롯·평균이 달라질 수 있으니, 확대는 coord_cartesian이 안전합니다.8. 저장 — ggsave()
화면이 아니라 파일로 저장할 땐 ggsave() 한 줄. 마지막에 그린 그림을 잡아 저장하며, 크기(인치)·해상도(dpi)를 직접 지정합니다. 논문·발표용은 dpi = 300 이상을 권장합니다.
p <- ggplot(expr, aes(group, log2expr, fill = group)) +
geom_boxplot() + facet_wrap(~ gene, nrow = 1)
ggsave("expr_boxplot.png", plot = p,
width = 9, height = 4, dpi = 300) # 9×4인치, 300dpi PNG
# 확장자만 바꾸면 형식이 바뀜: .pdf .svg .tiff ...
무엇이 보이나 — 작업 폴더에 expr_boxplot.png가 떨어집니다. 확장자만 .pdf·.svg로 바꾸면 벡터 그래픽으로 저장돼 무한 확대해도 안 깨집니다 — 논문 그림은 벡터가 정석입니다. plot =을 생략하면 가장 최근 그림이 저장됩니다.

geom 치트시트
자주 쓰는 기하 도형(geom)과 통계 변환을 한 표로 정리합니다.
| 함수 | 그림 | 주요 aes | 메모 |
|---|---|---|---|
geom_point() | 산점도 | x, y, color, shape, size | 두 연속변수의 관계 |
geom_jitter() | 흩뿌린 점 | x, y, color | 겹침 방지(범주형 x에 유용) |
geom_col() | 막대(값) | x, y, fill | y값을 그대로 높이로 |
geom_bar() | 막대(빈도) | x, fill | 행 개수를 셈(stat="count") |
geom_boxplot() | 박스플롯 | x, y, fill | 중앙값·사분위·이상치 |
geom_violin() | 바이올린 | x, y, fill | 분포 밀도까지 |
geom_line() | 선그래프 | x, y, color | 시계열·추세(linewidth) |
geom_histogram() | 히스토그램 | x, fill | 한 변수의 분포(bins) |
geom_smooth() | 추세선 | x, y | method="lm" 회귀선 |
stat_summary() | 요약 통계 | x, y | 평균±오차 막대 등 |
aes()의 단골 손님은 x·y·color(선·점 색)·fill(면 색)·shape(모양)·size(크기)·alpha(투명도)·group(연결 단위)입니다. 막대·박스는 fill, 점·선은 color라고 외워 두면 절반은 안 틀립니다.자주 발생하는 에러 & 해결
color가 안 먹히고 막대 테두리만 칠해짐 — 면을 칠하는 건fill, 선·점을 칠하는 건color입니다. 막대·박스는aes(fill = group)을 쓰세요.Error: unexpected '+'/ 그림이 한 겹만 나옴 —+를 줄 맨 앞에 뒀기 때문입니다.+는 항상 앞 줄 끝에 붙여, R이 "다음 줄로 이어진다"고 알게 하세요.- 그룹별로 색이 안 갈리고 전부 한 색 —
color를aes()밖에 적었습니다. 데이터에 따라 변해야 하면aes(color = group)처럼 안으로 옮기세요. - 막대·축 순서가 알파벳순으로 멋대로 — ggplot2는 문자형을 알파벳순으로 정렬합니다. 원하는 순서는
factor(x, levels = c(...))로 요인 수준을 먼저 지정하세요(이 글의Control→Treated처럼). - 색이 의도와 다르게 그라데이션으로 나옴 — 색 매핑 변수가 연속형(숫자)이면 그라데이션, 범주형(요인·문자)이면 구분색입니다. 범주로 쓰려면
factor()로 감싸세요. Error ... object 'depth' not found—aes()안의 이름은 데이터프레임의 열 이름이어야 합니다. 철자·대소문자를names(expr)로 확인하세요.
• (응용·시각화) Volcano plot 그리기 —
geom_point + 색 매핑 + 임계선으로 차등발현을 한 장에 · PCA로 샘플 QC — geom_point로 주성분 산점도• (히트맵) ComplexHeatmap 히트맵 — 발현 행렬을 색으로 (ggplot2와 다른 문법이지만 매핑의 발상은 같음)
• (데이터 소스) DESeq2 차등발현 · clusterProfiler 농축분석 — 여기서 나온 결과 표가 위 그래프의 입력이 됩니다
References
- Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis (2nd ed.). Springer. (그래픽 문법의 표준 교재)
- Wickham, H., et al. (2019). Welcome to the tidyverse. Journal of Open Source Software, 4(43), 1686.
- ggplot2 공식 문서 (tidyverse.org/ggplot2) —
aes·geom_*·facet_wrap·scale_*·theme·ggsave레퍼런스. - Wilkinson, L. (2005). The Grammar of Graphics (2nd ed.). Springer. (ggplot2가 따르는 그래픽 문법의 원전)
- Healy, K. (2018). Data Visualization: A Practical Introduction. Princeton University Press. (ggplot2 실전 시각화 안내)
Pipette & Pipeline · A bio portfolio journal
'Code Bench > R Snippets' 카테고리의 다른 글
| dplyr·tidyr로 생물 데이터 정리하기 — 분석 전 90%는 데이터 정리 (R 실전) (0) | 2026.06.18 |
|---|---|
| tximport로 salmon·kallisto 정량 불러오기 — RNA-seq 카운트의 출발점 (R 실전) (0) | 2026.06.17 |
| limma-voom으로 RNA-seq 차등발현(DEG) 분석하기 — DESeq2·edgeR과 무엇이 다른가 (R 실전) (0) | 2026.06.17 |
| GEOquery로 공개 발현 데이터 받아서 분석하기 — GEO에서 내 PC로, 재분석까지 (R 실전) (0) | 2026.06.17 |
| WGCNA로 유전자 공발현 네트워크 분석하기 — 모듈부터 허브 유전자까지 (R 실전) (0) | 2026.06.16 |