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. 데이터 → aes 매핑 → geom → scale/theme로 레이어를 쌓아 그림을 완성하는 ggplot2의 문법.

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 밖

이제 그룹을 색으로 구분해 봅시다. 그룹별로 색을 다르게 하려면 coloraes() 안에 넣습니다. 데이터 열에 따라 변하는 건 전부 aes() 안, 모든 점에 똑같이 적용할 고정값은 aes() 밖입니다.

ggplot(expr, aes(x = depth, y = log2expr,
                 color = group, shape = group)) +  # 그룹 → 색·모양 (매핑)
  geom_point(size = 3, alpha = 0.8)                # 크기·투명도는 고정값 (밖)

무엇이 보이나 — 대조군·처리군이 다른 색·다른 모양으로 갈리고, 오른쪽에 범례가 자동으로 생깁니다. 반면 size = 3·alpha = 0.8aes() 밖에 있으니 모든 점에 똑같이 적용되고 범례에 안 뜹니다. 이 안/밖 구분이 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를 나중에 둬야 점이 박스 위에 올라옵니다. 순서를 바꾸면 박스가 점을 덮어 버립니다.

그림 2. 같은 데이터라도 geom을 바꾸면 그림이 달라진다 — 점·막대·박스·라인의 쓰임.

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 vs coord — 축 자체를 로그로 바꾸려면 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 =을 생략하면 가장 최근 그림이 저장됩니다.

그림 3. 데이터 열을 어떤 aes에 연결하느냐에 따라 위치·색·모양·패널이 정해진다.

geom 치트시트

자주 쓰는 기하 도형⁠(geom)과 통계 변환을 한 표로 정리합니다.

함수 그림 주요 aes 메모
geom_point()산점도x, y, color, shape, size두 연속변수의 관계
geom_jitter()흩뿌린 점x, y, color겹침 방지⁠(범주형 x에 유용)
geom_col()막대⁠(값)x, y, filly값을 그대로 높이로
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, ymethod="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이 "다음 줄로 이어진다"고 알게 하세요.
  • 그룹별로 색이 안 갈리고 전부 한 색coloraes() 에 적었습니다. 데이터에 따라 변해야 하면 aes(color = group)처럼 으로 옮기세요.
  • 막대·축 순서가 알파벳순으로 멋대로 — ggplot2는 문자형을 알파벳순으로 정렬합니다. 원하는 순서는 factor(x, levels = c(...))요인 수준을 먼저 지정하세요⁠(이 글의 ControlTreated처럼).
  • 색이 의도와 다르게 그라데이션으로 나옴 — 색 매핑 변수가 연속형⁠(숫자)이면 그라데이션, 범주형⁠(요인·문자)이면 구분색입니다. 범주로 쓰려면 factor()로 감싸세요.
  • Error ... object 'depth' not foundaes() 안의 이름은 데이터프레임의 열 이름이어야 합니다. 철자·대소문자를 names(expr)로 확인하세요.
🎓 다음 학습
(응용·시각화) Volcano plot 그리기geom_point + 색 매핑 + 임계선으로 차등발현을 한 장에 · PCA로 샘플 QCgeom_point로 주성분 산점도
(히트맵) ComplexHeatmap 히트맵 — 발현 행렬을 색으로 (ggplot2와 다른 문법이지만 매핑의 발상은 같음)
(데이터 소스) DESeq2 차등발현 · clusterProfiler 농축분석 — 여기서 나온 결과 표가 위 그래프의 입력이 됩니다

References

  1. Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis (2nd ed.). Springer. (그래픽 문법의 표준 교재)
  2. Wickham, H., et al. (2019). Welcome to the tidyverse. Journal of Open Source Software, 4(43), 1686.
  3. ggplot2 공식 문서 (tidyverse.org/ggplot2) — aes·geom_*·facet_wrap·scale_*·theme·ggsave 레퍼런스.
  4. Wilkinson, L. (2005). The Grammar of Graphics (2nd ed.). Springer. (ggplot2가 따르는 그래픽 문법의 원전)
  5. Healy, K. (2018). Data Visualization: A Practical Introduction. Princeton University Press. (ggplot2 실전 시각화 안내)

Pipette & Pipeline · A bio portfolio journal

이 글을 쓴 사람 Yumingming

생명융합공학과 박사과정.
Microbiome · Cosmetics · RNA Therapeutics · Bioinformatics를 공부하며,
실험(Wet Lab)과 데이터(Dry Lab)를 잇는 글을 논문(article) 기반으로 씁니다.

About · 더 알아보기 →

⚕️ 이 글은 학습·정보 제공 목적이며, 의학적 진단·치료·조언을 대체하지 않습니다. 건강·질병·치료에 관한 결정은 반드시 의사 등 전문가와 상의하세요. 자세한 내용은 면책조항을 참고해 주세요.