聰明不如鈍筆
총명불여둔필
assignment KindeR

ggplot으로 '세상은 정규분포' 짤 그리기(feat. fun=dnorm)


인터넷을 돌아다니다 보면 '세상은 정규분포'라는 유명한 짤(그림)과 마주칠 때가 있습니다.  


원래 처음에는 트위터에 '주변 사람들이 구글만 쓰던데 네이버는 누가 쓰죠?'라는 질문에 대한 대답으로 등장한 그림입니다.


물론 이 세상 모든 일이 정규분포를 따르는 건 아니지만 이 분포를 알고 있으면 세상을 이해하는 데 도움이 되는 건 사실입니다. 예를 들어 메이저리그 스카우트가 20~80점 사이로 선수를 평가하는 이유도 이 정규분포에서 찾을 수 있습니다. (사실 이건 누군가 처음에 원인과 결과를 착각해서 생긴 일입니다. 결과적으로 정규분포를 따르게 되는 건데 정규분포 특성을 기준으로 평가를 내리고 있으니 말입니다.)


그런데 도대체 정규분포가 뭘까요? 다들 학창시절에 이 분포가 뭔지 배우셨겠지만 잊은 분이 많으실 테니 (이 블로그의 아주 좋은 친구) 위키피디아를 통해 기억을 되살려 보면:


확률론통계학에서, 정규 분포(正規 分布, 영어: normal distribution) 또는 가우시안 분포(Gauß 分布, 영어: Gaussian distribution)는 연속 확률 분포의 하나이다. 정규분포는 수집된 자료의 분포를 근사하는 데에 자주 사용되며, 이것은 중심극한정리에 의하여 독립적인 확률변수들의 평균은 정규분포에 가까워지는 성질이 있기 때문이다.


정규분포는 2개의 매개 변수 평균 $\mu$과 표준편차 $\sigma$에 대해 모양이 결정되고, 이때의 분포를 $N(\mu, \sigma^{2})$로 표기한다. 특히, 평균이 0이고 표준편차가 1인 정규분포 $N(0, 1)$을 표준 정규 분포(standard normal distribution)라고 한다.


여기서 첫 번째로 중요한 건 '독립적인 확률변수들의 평균은 정규분포에 가까워지는 성질이 있다'는 내용입니다. 이게 우리가 정규분포를 알고 있어야 하는 이유라고 할 수 있습니다. 이 성질을 이용한 장난감도 있습니다.



'가우시안 분포'라는 표현에도 주목할 만합니다. 정규 분포를 이런 이름으로 부르기도 하는 건 독일 수학자 카를 프리드리히 가우스(1777~1855)가 이 개념을 처음 정립했기 때문. 이게 얼마나 자랑스러운 일이었는지 독일 정부는 2002년 유로화를 도입하기 이전에 쓰던 10 마르크 지폐(아래 사진)에 가우스 얼굴은 물론 정규분포 그래프와 수식을 넣었습니다.



이 정도면 정규분포가 무엇이고 얼마나 중요한지 감을 잡으셨을 테니 R 코드로 넘어가도록 하겠습니다.



R에서 정규분포 그래프 그리기

R로 어떤 문제를 해결하는 데 정답이 하나뿐인 확률은 제로(0)라고 해도 과언이 아닙니다. 'R로 정규분포 그래프 그리기'도 마찬가지입니다. 이 포스트는 ggplot 패키지를 활용법에 초점을 맞춘 만큼 그 생태계 안에서 문제에 접근하도록 하겠습니다.


10 마르크 지폐에 나온 수식을 다시 쓰면 아래와 같습니다.


$f(x)=\frac{1}{\sigma\sqrt{2\pi}}e^{-\frac{{(x-\mu)}^{2}}{2\sigma^{2}}}$


이런 수식을 다른 말로는 확률 밀도 함수라고 부릅니다. x에 계산하고 싶은 값을 넣으면 f(x)에 결과가 나오는 방식입니다.


학창시절에는 이럴 때 실제로 x에 값을 몇 개 넣어 계산한 다음 선을 이어서 그래프를 그리지만 ggplot에서는 그럴 필요가 없습니다. stat_function()이라는 명령어(함수)가 존재하기 때문입니다. 이 명령어는 기본적으로 아래 형태로 씁니다.

ggplot(NULL, aes(c(x범위)) + stat_function(fun=함수)


R는 정규분포와 관련해 네 가지 함수를 제공합니다.


 함수  종류  설명
 rnorm  난수 함수  무작위로 y값 생성
 dnorm  확률 밀도 함수  x값에 해당하는 y값 산출
 pnorm  누적 분포 함수  x값에 따른 누적 확률 계산
 qnorm  분위수 함수  정규분포 확률에 해당하는 x값 제시


그럼 우리가 활용해 하는 함수는 무엇일까요? 네, 맞습니다. 정답은 dnorm입니다. 그러면 코드를 이렇게 쓰면 x가 -5~5일 때 정규분포 그래프가 나올 겁니다.

library(ggplot2)
ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm)


예쁘게 잘 나왔습니다. 이 그래프는 평균이 0, 표준편차가 1인 $N(0, 1)$, 그러니까 표준정규분포를 나타냅니다. 만약 평균과 표준편차가 다른 정규분포 그래프를 그리고 싶을 때는 어떻게 하면 될까요?


아래는 x가 0부터 100일 때 평균은 50, 편차가 10인 $N(50, 100)$ 그래프를 그리는 코드와 결과물입니다.

ggplot(NULL, aes(c(0, 100))) + 
  stat_function(fun=dnorm, args=list(mean=50, sd=10))


이미 보신 것처럼 함수 인자(args·arguments) 안에 리스트(list) 형태로 평균(mean)과 표준편차(sd)를 넣어주면 그만입니다.


여기서 상위 10%에만 색을 칠하고 싶을 때는 어떻게 할까요? 이 정규분포에서 상위 10%에 해당하는 값은 이런 코드로 확인할 수 있습니다.

qnorm(0.9, 50, 10)
[1]62.81552


10%인데 0.1이 아니라 0.9가 들어가는 건 '상위'라 그렇습니다. 이게 무슨 뜻인지 다들 이해하셨을 줄로 믿습니다.


ggplot에서 특정 영역(area)에 색을 칠할 때는 geom_area() 함수를 쓰는 방법도 있습니다. 이 함수 안에 함수를 넣을 때는 "stat='function'"이라는 속성을 넣어주면 됩니다. 위에서 보신 것처럼 'fun=dnorm, args=list(mean=50, sd=10)'도 들어가야겠죠? x값 범위는 qnorm(0.9, 50, 100)부터 100까지입니다. 색은 'gray75'로 하겠습니다. 이 내용을 코드로 쓰면 이렇게 나옵니다.

ggplot(NULL, aes(c(0, 100))) +
  geom_area(stat='function', fun=dnorm, args=list(mean=50, sd=10), xlim=c(qnorm(.9, 50, 10), 100), fill='gray75') +
  stat_function(fun=dnorm, args=list(mean=50, sd=10))


혹시 geom_area()를 왜 stat_function()보다 먼저 썼는지 궁금해 하시는 분이 계실지도 모르겠습니다. 그건 ggplot2가 앞에 나온 코드를 먼저 그리기 때문입니다. 영역이 아래 깔리고 선이 그 뒤를 덮어야 모양이 더 예쁘게 나올 것이라고 생각했던 겁니다.


마지막으로 실생활에서 이 정규분포 그래프를 한 번 활용해 볼까요?


문화체육관광부 '국민체력실태조사'에 따르면 2017년 기준으로 30~34세 성인 남성 키는 평균 174.6㎝에 표준편차 5.95㎝입니다. 그러면 180㎝ 이상은 상위 몇 %에 속할까요?


이를 알아보려면 pnorm() 함수를 쓰면 됩니다. 이렇게 말입니다.

pnorm(180, mean=174.6, sd=5.95)
[1] 0.8179454


이번에도 상위니까 1에서 이 값을 빼야 우리가 원하는 결과가 나옵니다. 키 180㎝는 상위 18.2%에 해당합니다. 


이 내용을 그래프로 그리고 주석까지 써넣으려면 코드를 이렇게 쓰면 됩니다. 텍스트 주석을 넣을 때는 geom_text() 함수를 쓰시면 됩니다.

ggplot(NULL, aes(c(150, 200))) +
  geom_area(stat='function', fun=dnorm, args=list(mean=174.6, sd=5.95), xlim=c(180, 200), fill='gray75') +
  stat_function(fun=dnorm, args=list(mean=174.6, sd=5.95)) +
  geom_text(aes(x=192, y=.02), label="←180㎝ 이상은 상위 18.2%")


자, 여기까지 오면서 이해가 가지 않는 내용 없으셨죠? 그럼 잠시 쉬시고 나서 '세상은 정규분포' 짤 그리기에 도전해 보도록 하겠습니다.



'세상은 정규분포' 짤을 ggplot으로 그려보자

이 짤을 보신 지 시간이 흘렀으니까 다시 한 번 보겠습니다. 어떤 요소를 포함해 그래프를 그려야 할지 잠시 생각해 보시는 것도 나쁘지 않을 겁니다.



• 일단, 정규분포 그래프: 지금까지 실컷 그렸습니다. 여기서는 맨 처음에 그린 버전을 쓰겠습니다.


• 파란 네모: ggplot에는 네모를 그릴 때 쓰는 geom_rect() 함수가 들어 있습니다.


• 빨간 화살표: 이럴 때 쓰라고 geom_segment() 함수가 있습니다. 여기에 arrow() 속성을 더하면 됩니다.


• 텍스트: 위에서 보신 것처럼 geom_text()를 쓰면 됩니다.


1단계는 정규분포 그래프 그리고 그 위에 파란 (웹 컬러 코드: #37638c) 네모 그리기. 코드로 쓰면 이렇습니다.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5)


응? 생각처럼 나오지 않았습니다. 네모가 y축 범위를 벗어나서 그렇습니다. 이럴 때는 y축 속성을 바꾸는 구실을 하는 scale_y_continuous() 함수에 limits 속성을 넣어주면 됩니다. 코드를 다시 써볼까요?

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  scale_y_continuous(limits=c(-.175, .525))


잘 나왔습니다. 그래프를 그리다가 화면이 잘린 듯한 느낌이 들 때는 이렇게 범위를 조정하시면 됩니다. x축이 문제라면 scale_x_continuous()를 쓰시면 되겠죠?


이제 빨간 화살표를 하나 그려넣겠습니다. 앞서 말씀드렸듯이 이번에는 geom_segment()를 쓸 겁니다. 이 함수는 이런 식으로 씁니다.

geom_segment(aes(x=x시작점, xend=x끝점, y=y시작점, yend=y끝점))


우리는 x=2를 기준으로 선을 그렸으니까 그보다 큰 x를 하나 골라서 바깥으로 선을 그리면 되겠죠? 만약 x=2.2를 고른다면 정규분포 그래프 위에 있는 y값을 뭐가 될까요? 네, dnorm(2.2)가 될 겁니다. 화살표가 바깥쪽에서 이 점을 향해야 하니까 이 지점이 각각 xend, yend에 자리잡을 겁니다. 이 그래프 바깥에 있는 적당한 지점을 x, y에 넣어서 'darkred' 색으로 선을 하나 그려보겠습니다.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), color='darkred') +
  scale_y_continuous(lim=c(-.175, .525))


화살표가 빠졌네요. 화살표는 arrorw() 속성을 넣으면 된다고 했으니까 코드를 이렇게 다시 쓰고 그래프를 그려보겠습니다. 선 굵기(lwd)도 1로 맞췄습니다.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow()) +
  scale_y_continuous(lim=c(-.175, .525))


원본 '짤'에서는 화살표가 더 크지만 저는 이렇게 큰 화살표가 마음에 들지 않습니다. 그래서 이 화살표 크기를 줄이고 싶습니다. 그럴 때는 arrow() 안에 length 속성을 조절하면 됩니다. 저는 "length=unit(0.03, 'npc')"라고 써넣겠습니다. npc는 'Normalised Parent Coordinates'를 줄인 말로 R에서 쓰는 기본 (길이) 단위입니다.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc'))) +
  scale_y_continuous(lim=c(-.175, .525))


첫 번째를 그렸으면 두 번째는 못 그릴 게 없겠죠? 같은 형식으로 자리만 바꿔서 그려줍니다.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc'))) +
  geom_segment(aes(x=3.5, xend=2.4, y=dnorm(2.4)+0.1, yend=dnorm(2.4)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc')), ) +
  scale_y_continuous(lim=c(-.175, .525))


자, 이제 글씨를 쓰도록 하겠습니다. 위에서 보신 것처럼 geom_text()는 위치를 x, y값으로 잡아주고 label 안에 쓰고 싶은 글씨를 넣어주면 됩니다. 어려울 게 없으니 한번에 후다닥.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc'))) +
  geom_segment(aes(x=3.5, xend=2.4, y=dnorm(2.4)+0.1, yend=dnorm(2.4)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc')), ) +
  geom_text(aes(x=2.85, y=dnorm(2.2)+0.230), label='여러분 친구', size=4) +
  geom_text(aes(x=3.95, y=dnorm(2.4)+0.110), label='여러분', size=4) +
  geom_text(aes(x=0, y=.460, label='Real World'), size=6) +
  geom_text(aes(x=0, y=-.125, label='Normal Distribution')) +
  geom_text(aes(x=0, y=-.035), label='m') +
  geom_text(aes(x=-1, y=-.035), label='-σ') +
  geom_text(aes(x=1, y=-.035), label='+σ') +
  scale_y_continuous(lim=c(-.175, .525))


글씨 중에 제목만 빠졌습니다. ggtitle()을 쓰면 ggplot 그래프에 제목을 붙일 수 있습니다.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc'))) +
  geom_segment(aes(x=3.5, xend=2.4, y=dnorm(2.4)+0.1, yend=dnorm(2.4)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc')), ) +
  geom_text(aes(x=2.85, y=dnorm(2.2)+0.230), label='여러분 친구', size=4) +
  geom_text(aes(x=3.95, y=dnorm(2.4)+0.110), label='여러분', size=4) +
  geom_text(aes(x=0, y=.460, label='Real World'), size=6) +
  geom_text(aes(x=0, y=-.125, label='Normal Distribution')) +
  geom_text(aes(x=0, y=-.035), label='m') +
  geom_text(aes(x=-1, y=-.035), label='-σ') +
  geom_text(aes(x=1, y=-.035), label='+σ') +
  ggtitle(label='세상은 정규분포') +
  scale_y_continuous(lim=c(-.175, .525))


고객님 많이 당황하셨죠? 제목이 우리 기대와 달리 가운데 듬직하게가 아니라 왼쪽에 얌전히 자리잡고 있습니다.


이럴 때는 theme() 함수 안에 plot.title() 속성을 조절하면 됩니다. 수평 정렬은 hjust를 쓰면 되고, 우리는 가운데 정렬을 하고 싶으니까 'hjust=0.5'라고 값을 주면 됩니다. 기왕 조절하는 김에 글씨 크기도 키우고(size=24), 글꼴도 두껍게(face='bold') 만들겠습니다.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc'))) +
  geom_segment(aes(x=3.5, xend=2.4, y=dnorm(2.4)+0.1, yend=dnorm(2.4)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc')), ) +
  geom_text(aes(x=2.85, y=dnorm(2.2)+0.230), label='여러분 친구', size=4) +
  geom_text(aes(x=3.95, y=dnorm(2.4)+0.110), label='여러분', size=4) +
  geom_text(aes(x=0, y=.460, label='Real World'), size=6) +
  geom_text(aes(x=0, y=-.125, label='Normal Distribution')) +
  geom_text(aes(x=0, y=-.035), label='m') +
  geom_text(aes(x=-1, y=-.035), label='-σ') +
  geom_text(aes(x=1, y=-.035), label='+σ') +
  ggtitle(label='세상은 정규분포') +
  scale_y_continuous(lim=c(-.175, .525)) +
  theme(plot.title=element_text(hjust=0.5, size=24, face='bold'))

갈수록 비슷해 지고 있습니다. 이번에는 배경에 있는 격자무늬를 없애 볼까요? ggplot에는 '레디 메이드(ready made)' 테마 여럿가 들어 있는데 그 중에 void 테마가 비슷한 느낌을 줄 것 같습니다.


이렇게 미리 만들어 놓은 테마를 쓸 때는 '+theme_테마 이름'을 더하시면 됩니다. 단, 이렇게 쓰면 미리 theme()으로 조절해 놓은 것까지 테마 디자인이 덮어쓸 수 있습니다. 그러니 theme() 앞에 코드를 쓰시면 도움이 될 수 있습니다.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc'))) +
  geom_segment(aes(x=3.5, xend=2.4, y=dnorm(2.4)+0.1, yend=dnorm(2.4)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc')), ) +
  geom_text(aes(x=2.85, y=dnorm(2.2)+0.230), label='여러분 친구', size=4) +
  geom_text(aes(x=3.95, y=dnorm(2.4)+0.110), label='여러분', size=4) +
  geom_text(aes(x=0, y=.460, label='Real World'), size=6) +
  geom_text(aes(x=0, y=-.125, label='Normal Distribution')) +
  geom_text(aes(x=0, y=-.035), label='m') +
  geom_text(aes(x=-1, y=-.035), label='-σ') +
  geom_text(aes(x=1, y=-.035), label='+σ') +
  ggtitle(label='세상은 정규분포') +
  scale_y_continuous(lim=c(-.175, .525)) +
  theme_void() +
  theme(plot.title=element_text(hjust=0.5, size=24, face='bold'))


배경을 지웠더니 y=0 선이 없는 게 눈에 띕니다. 그래프 전체를 가로지르는 가로 선을 그을 때는 geom_hline()을 쓰시면 됩니다. (참고로 수직선은 geom_vline()입니다. ) 이 함수 안에 yintercept(수직선은 xintercept)를 넣어서 위치를 표시합니다. 그러면 우리가 필요로 하는 건 'geom_hline(yintercept=0)'이 되겠죠?

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc'))) +
  geom_segment(aes(x=3.5, xend=2.4, y=dnorm(2.4)+0.1, yend=dnorm(2.4)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc')), ) +
  geom_text(aes(x=2.85, y=dnorm(2.2)+0.230), label='여러분 친구', size=4) +
  geom_text(aes(x=3.95, y=dnorm(2.4)+0.110), label='여러분', size=4) +
  geom_text(aes(x=0, y=.460, label='Real World'), size=6) +
  geom_text(aes(x=0, y=-.125, label='Normal Distribution')) +
  geom_text(aes(x=0, y=-.035), label='m') +
  geom_text(aes(x=-1, y=-.035), label='-σ') +
  geom_text(aes(x=1, y=-.035), label='+σ') +
  ggtitle(label='세상은 정규분포') +
  geom_hline(yintercept=0) +
  scale_y_continuous(lim=c(-.175, .525)) +
  theme_void() +
  theme(plot.title=element_text(hjust=0.5, size=24, face='bold'))


이제 디테일이 남았습니다. 배경(plot.background)을 옅은 회색(gray95)으로 칠하고, 제목을 조금 더 아래로 내리고(vjust) 싶습니다. y=0 수평선도 조금 더 축처럼 보이게 만들어 볼까요?

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  geom_rect(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5, color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc'))) +
  geom_segment(aes(x=3.5, xend=2.4, y=dnorm(2.4)+0.1, yend=dnorm(2.4)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc')), ) +
  geom_text(aes(x=2.85, y=dnorm(2.2)+0.230), label='여러분 친구', size=4) +
  geom_text(aes(x=3.95, y=dnorm(2.4)+0.110), label='여러분', size=4) +
  geom_text(aes(x=0, y=.460, label='Real World'), size=6) +
  geom_text(aes(x=0, y=-.125, label='Normal Distribution')) +
  geom_text(aes(x=0, y=-.035), label='m') +
  geom_text(aes(x=-1, y=-.035), label='-σ') +
  geom_text(aes(x=1, y=-.035), label='+σ') +
  ggtitle(label='세상은 정규분포') +
  geom_hline(yintercept=0) +
  geom_segment(aes(x=-1, xend=-1, y=-.025, yend=0)) +
  geom_segment(aes(x=0, xend=0, y=-.025, yend=0)) +
  geom_segment(aes(x=1, xend=1, y=-.025, yend=0)) +
  scale_y_continuous(lim=c(-.175, .525)) +
  theme_void() +
  theme(plot.title=element_text(hjust=0.5, vjust=-2.5, size=24, face='bold'), plot.background =element_rect(fill='gray95'))


예, 이 정도면 완성이라고 해도 과언이 아닙니다. 그런데 이상하게 아까부터 이 파란 네모 모서리가 둥근 모양이 아닌 게 마음에 걸립니다.


ggplot에는 이렇게 사격형 모서리를 둥글게 하는 함수는 존재하지 않습니다. 대신 원래 미국 50개 주 통계 비교용 패키지 'statebins'에 geom_rrect() 함수가 들어 있는 게 생각이 났습니다. 이 패키지는 R 공식 패키지 저장소인 CRAN(the Comprehensive R Archive Network)에 들어 있지 않기 때문에 깃허브에서 내려받아야 합니다.


깃허브에서 패키지를 내려 받을 때는 devtools 패캐지에 들어 있는 install_github() 함수가 필요합니다. 아직 설치를 하지 않으셨다면 아래처럼 진행합니다.

install.packages('devtools')
library('devtools')
install_github('hrbrmstr/statebins')
library('statebins')


이제 원래 geom_rect()였던 부분을 staebins:::geom_rrect()로 고치면 됩니다. 함수 앞에 :::를 쓰면 그 앞에 있는 패키지에서 함수를 불러옵니다.

ggplot(NULL, aes(c(-5, 5))) + stat_function(fun=dnorm) +
  statebins:::geom_rrect(mapping=aes(xmin=-2, xmax=2, ymin=-0.175, ymax=0.5), color='#37638c', fill='transparent', lwd=1.5) +
  geom_segment(aes(x=2.6, xend=2.2, y=dnorm(2.2)+0.2, yend=dnorm(2.2)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc'))) +
  geom_segment(aes(x=3.5, xend=2.4, y=dnorm(2.4)+0.1, yend=dnorm(2.4)), lwd=1, color='darkred', arrow=arrow(length = unit(0.03, 'npc')), ) +
  geom_text(aes(x=2.85, y=dnorm(2.2)+0.230), label='여러분 친구', size=4) +
  geom_text(aes(x=3.95, y=dnorm(2.4)+0.110), label='여러분', size=4) +
  geom_text(aes(x=0, y=.460, label='Real World'), size=6) +
  geom_text(aes(x=0, y=-.125, label='Normal Distribution')) +
  geom_text(aes(x=0, y=-.035), label='m') +
  geom_text(aes(x=-1, y=-.035), label='-σ') +
  geom_text(aes(x=1, y=-.035), label='+σ') +
  ggtitle(label='세상은 정규분포') +
  geom_hline(yintercept=0) +
  geom_segment(aes(x=-1, xend=-1, y=-.025, yend=0)) +
  geom_segment(aes(x=0, xend=0, y=-.025, yend=0)) +
  geom_segment(aes(x=1, xend=1, y=-.025, yend=0)) +
  scale_y_continuous(lim=c(-.175, .525)) +
  theme_void() +
  theme(plot.title=element_text(hjust=0.5, vjust=-2.5, size=24, face='bold'), plot.background =element_rect(fill='gray95'))


자, 이제 정말 드디어 끝이 났습니다. 고생 많으셨습니다. 여기까지 잘 따라하셨다면 스스로에게 박수를 쳐주셔도 좋습니다.



코드 훔치기는 선택, 노가다는 필수

처음 이런 글을 써야겠다고 마음 먹은 건 'ggplot 산점도 위에 회귀선(식) 표시하기(feat. geom_text)' 포스트를 쓰면서 rnorm() 함수를 이용했다는 게 제일 큰 이유였습니다.


그런데 여기까지 오셨다면 정규분포 그래프 자체보다는 ggplot으로 그래프를 완성하는 과정을 한 번 더 보여드리는 데 이 글 의도가 있었다는 걸 눈치채셨을 줄로 믿습니다.


'남이 쓴 ggplot 코드 훔쳐서 움직이는 막대 그래프 그리기(feat. gganimate)'를 통해 말씀드린 것처럼 세상에는 이미 (원하시는 수식어를 넣으시오) ggplot 코드가 많이 있습니다. 이런 코드를 발견하시면 얼마든 원하는 그래프를 그리실 수 있습니다.


단, 그때도 미세한 부분을 조정하시려면 수정 작업을 필수고, 이런 수정 작업은 대부분 흔히 '노다가'라고 부르는 반복 작업일 때가 많습니다. 반복이 귀찮은 건 사실이지만 아주 못할 일은 아니잖아요?


모두들 원하시는 그래프를 그리는 데 이 글이 조금이라도 도움이 되기를 바라면서 글을 마칩니다.그럼 여러분 모두, Happy Charting!


댓글,

KindeR | 카테고리 다른 글 더 보기