matplotlib의 기본 사용법 및 다른 시각화 라이브러리

오늘은 파이썬의 시각화 부분에서 가장 널리 쓰이고 있는 matplotlib에 대해서 알아보도록 하자.

matplotlib을 사용할 때 주로 서브패키지인 pyplot을 사용한다. pyplot은 MATLAB의 인터페이스와 유사하게 작동할 수 있도록 하여 MATLAB을 사용하는 사용자층이 쉽게 matplotlib으로 옮겨오도록 하고 있다. 문제는 MATLAB의 인터페이스가 그모양이어서 그런지 모르겠는데, matplotlib의 인터페이스가 일관성도 없는 편이고 그다지 객체지향적이지도 않아서 사실상 API 문서만으로도 사용이 어렵고 관련 예제를 보면서 코드를 따라써야 하는 수준으로 처리해야 하는 경우가 많다는 것이다.

결국 matplotlib 공식 홈페이지 내의 Example 섹션을 보고, 만들고자하는 차트와 비슷한 것이 있으면 그 예제의 코드를 가져와서 사용하는 것이 가장 좋은 방법이라는 결론. 그리고 그럼에도 불구하고 용어들의 네이밍 센스가 기가막히는 수준이기 때문에 몇 가지는 알아두는 것이 좋겠다.

https://matplotlib.org/3.1.1/gallery/index.html


기본개념 및 용어

matplotlib에서 시각화 대상에 대해 가리키는 몇 가지 용어를 이해하자. 우리는 흔히 ‘plot’을 주로 사용하는데 matplotlib 문서에서는 다음과 같은 이름을 사용한다. 공식 문서를 사용할 때나 API에 접근할 때 참고하자. 그나저나 네이밍 센스 하나 만큼은 정말 처참한 수준이다.

  • Figure : matplotlib은 한 번에 한장의 그림을 그린다. 이 그림을 가리키는 용어가 figure이다. figure는 그림을 그리는 캔버스로 생각할 수 있으며, 하나의 figure에는 여러 개의 plot이 들어갈 수 있다. plot이 하나의 차트에 해당한다. (그런데….?)
  • Axes : 보통 plot으로 생각하는 하나의 그래프. 각각의 Axes는 개별적으로 제목 및 x/y 레이블을 가질 수 있다. (왜 플롯이나 차트라고 하지 않는지…)
  • Axis : 번역하면 ‘축’인데, 그려지는 축을 말하는 것이 아니라 정확히는 x, y 의 제한 범위를 말한다.
  • Artist : (작명 센스봐라 – _-) Figure 위에 그려지는 모든 것들은 각각 artist라 한다. 텍스트, 라인 같은 것들. 이들 대부분은 자신이 그려지는 Axes에 묶여 있다고 볼 수 있다. (대부분이라는 것은 일부는 또 아니라는 말이다…)

기본적인 선 그래프 그리기

선 그래프는 plot() 으로 그릴 수 있다. 간단한 선 그래프를 그려보자. matplotlib의 하위 패키지인 pyplot을 불러와서 pyplot.plot(x, y)의 형태로 플롯을 그린다. x, y는 각 축에 대응하는 값의 집합으로 파이썬 리스트나 numpy array를 사용한다.

import numpy as np
import matplotlib.pyplot as plt

x = np.array([1,2,3,4])
y = x ** 2
plt.plot(x, y)
plt.show()

plt.{그래프형식}(...)의 모양으로 실행되는 명령은 모두 현재 컨텍스트에 그래프를 그리는 명령이다. (자동으로 하나 생성되거나 아니면 원래 하나 있거나 그런 듯.) 그리고 그리는 작업이 모두 끝나면 plt.show()를 사용해서 결과물을 렌더링한다.

익숙해지면 간단하다고 생각할지 모르지만, 여러 가지로 마음에 들지 않는다. 1) 일단 뭔가 백그라운드에 이런 저런 그래픽 컨텍스트가 나도 모르게 하나 존재한다. 2) plot = plt.plot(); plot.show() 라거나 plt.currentContext.show() 가 아니라 plt.show()이다. 이거 무슨 마치 PHP도 아니고….

플롯을 그린 후 plt.show()를 사용하기 전까지는 제목이라든지 시각 요소들을 바꾸거나 설정할 수 있다. 뭔가 show()를 호출하고나면 현재의 컨텍스트가 flush()되는 구조인 듯 하다. 다음과 같이 다시 차트를 생성하고 타이틀을 붙인 후에 렌더링할 수 있다.

plt.plot(x, y)
plt.xlabel('X label')  # x축 아래에 레이블을 그린다.
plt.ylabel('Y label')  # y축 밖에 레이블을 그린다.
plt.text(3, 8, '$y=x^2$')  # (3, 8) 위치에 텍스트르를 그린다.
# 텍스트는 $~$ 사이에 넣어서 LateX 수식을 표현할 수 있다.
plt.show()

그림의 크기는 plt.figure() 함수를 통해서 조절할 수 있다. plt.figure()는 figure의 속성을 변경하는데 사용되는 함수이다. 크기를 변경하려면 plt.figure(figsize=(15, 5)) 와 같이 가로/세로 크기를 준다. 이 때 단위는 ‘인치’이다. (너네 과학/공학용 패키지 아니니?) dpi= 옵션을 추가로 사용해서 픽셀 크기를 조정할 수 있다. (이러한 옵션의 기본값은 matplotlib.rcParam 값을 변경하여 디폴트값을 바꿔줄 수 있다고 한다.)


그래프의 모양 변경하기

플롯은 항상 선 그래프만 가능한 것은 아니다. plot() 함수에 인자를 넘겨줄 때에는 x값 데이터, y 값 데이터 외에 추가로 그래프의 형식을 지정할 수 있는데, ‘색+모양’으로 이루어진 약어를 쓴다. 기본적으로 plot() 함수는 파란색 선 그래프를 그리는데, 이는 이 기본값이 'b-'이라는 이야기이다. ‘go’로 변경하면 다음과 같이 점 그래프로 변경할 수 있다. green 이고 ‘o’는 동그라미 점이라는 뜻인가 보다.

plt.plot(x, y, 'go') # green / dot 
plt.show()

또한 여러 세트의 데이터를 한 번에 전달하여 하나의 axes에 그릴 수 있다. x데이터, y데이터, 형식,…의 반복으로 할 수 있다.

plt.plot(x, x, 'go', x, y, 'r^', x, x**3, 'y*')

plt.plot()은 현재 컨텍스트 (혹은 figure?)에 플롯 그래프를 그린다는 뜻이다. .xlabel(), .ylabel(), .text()와 같은 ‘드로잉’ 메소드이다. 위와 같이 데이터를 한꺼번에 그릴 수도 있고 다음과 같이 여러 플롯을 겹쳐 그릴 수도 있다.

plt.plot(x, y)
plt.plot(x, y, 'go')
plt.show()

figure 하나에 여러 플롯 그리기

subplot() 명령을 사용하면 하나의 figure 내에 여러 그래프를 같이 그릴 수 있다. 이 명령은 세 개의 인자를 받는데 (행의 수, 열의 수, 인덱스)가 된다. 이는 행, 열의 수만큼 구획을 나누고 인덱스가 가리키는 영역에 그림을 그린다는 뜻이다.

plt.subplot(1, 2, 1) # 서브플롯을 나누고 1번 플롯으로 문맥을 전환한다.
plt.plot(x, y, 'r-', x, y, 'go')
plt.title('1st subplot')

plt.subplot(1, 2, 2)  # 서브플롯을 나누고 이제 2번 플롯을 다룬다.
plt.plot(x, x**3, 'g-', x, x**3, 'r^')
plt.title('2nd subplot')

plt.show()

plt.suptitle('Subplots') # 전체 타이틀 추가
plt.show()

subplots 명령

만약 그려야하는 플롯이 많을 때에는, 위의 방식은 조금 지저분해보일 수 있다. 이를 위해 subplots() 함수가 (아놔, 뒤에 s 붙인 걸로 구분 끝 ^^;;) 별도로 제공되는데 행, 열의 개수를 인자로 받아서 (figure, axes)의 쌍을 리턴한다. (axes는 도끼가 아니라 plot 하나를 말한다!) axes는 다시 2차원 np 배열로 각각의 영역을 가리킨다.

x = np.linspace(0.1, 2, 50)
plt.suptitle('2x2 Plots')
# 현재 컨텍스트를 2x2 구간으로 나눈다. 
# fig는 전체 figure?
# axs는 2x2의 배열이며, 각 차트에 대응한다. 
fig, axs = plt.subplots(2, 2, sharex=True) 
# sharex 옵션은 아래/위 그래프가 x축을 공유한다는 의미이다.

axs[0,0].plot(x, x)
axs[0,0].text(1.2, 1, '$y=x$')

axs[0,1].plot(x, x**2)
axs[0,1].text(1.2, 1, '$y=x^2$')

axs[1,0].plot(x, 1/x)
axs[1,0].text(1.2, 3, r'$y=\frac{1}{x}$')

axs[1,1].plot(x, x**3)
axs[1,1].text(0.8, 3, r'$y=x^3$')

plt.show()

여러 그래프를 그릴 때 각각의 서브플롯에 제목을 달게되면, 제목과 다른 플롯의 축이 겹치는 불상사가 발생한다. 이를 방지하기 위해서 constrained_layout=True 옵션을 사용하면 서브 플롯간의 간격을 확보하여 이런 불상사를 방지할 수 있다.

def f(t):
    s1 = np.cos(2*np.pi*t)
    e1 = np.exp(-t)
    return s1 * e1

t1 = np.arange(0.0, 5.0, 0.1)
t2 = np.arange(0.0, 5.0, 0.02)
t3 = np.arange(0.0, 2.0, 0.01)

# constrained_layout=True를 사용해야 타이틀과 축이 겹치지 않는다.
fig, axs = plt.subplots(2, 1, constrained_layout=True, sharey=True)
fig.suptitle('This is a somewhat long figure title', fontsize=16)

axs[0].plot(t1, f(t1), 'o', t2, f(t2), '-')
axs[0].set_title('subplot 1')
axs[0].set_xlabel('distance (m)')
axs[0].set_ylabel('Damped oscillation')


axs[1].plot(t3, np.cos(2*np.pi*t3), '--')
axs[1].set_xlabel('time (s)')
axs[1].set_title('subplot 2')
axs[1].set_ylabel('Undamped')
axs[1].grid(True)            # 격자 표시
axs[1].set_xlim([0, 2])      # x축의 범위를 제한

plt.show()

다른 차트 종류

바 그래프

pyplot이 제공하는 bar() 함수를 통해 막대 그래프를 그릴 수 있다. hbar() 함수를 통해서는 가로형 막대 그래프를 그릴 수 있다.

x = np.arange(8)
y = 10 + 3 * np.random.randn(8)
plt.bar(x, y)
plt.show()

다음은 hbar()를 사용해서 가로형 막대 그래프를 그리는 예이다. 이번엔 특이하게 y축의 각 항목이 숫자값이 아닌 별도의 레이블로 정의된다. 각 데이터에 대한 올바른 레이블을 붙이기 위해서 plt.subplots()를 사용해서 단일 차트용 axes를 얻은 다음, set_yticks(), set_yticklabels()를 사용한다.

(https://matplotlib.org/3.1.1/gallery/lines_bars_and_markers/barh.html#sphx-glr-gallery-lines-bars-and-markers-barh-py)

np.random.seed(13524545)
plt.rcdefaults()
fig, ax = plt.subplots() # ???

people = ('Tom', 'Dick', 'Harry', 'Slim', 'Jim')
y_pos = np.arange(len(people))
# 평균3, 표준편차 10의 정규분포에서 난수생성
performace = 3 + 10 * np.random.rand(len(people))
error = np.random.rand(len(people))

ax.barh(y_pos, performace, xerr=error)
ax.set_yticks(y_pos)
ax.set_yticklabels(people)
ax.invert_yaxis()
ax.set_xlabel('Peformance')
ax.set_title('How fast do you want to go today?')

plt.show()

막대 그래프를 그릴 때 bottom= 파라미터로 값을 전달하면 막대 그래프를 위로 올려서 그릴 수 있다. 이를 이용하면 누적 막대 그래프를 그릴 수 있다.

https://matplotlib.org/3.1.1/gallery/lines_bars_and_markers/bar_stacked.html#sphx-glr-gallery-lines-bars-and-markers-bar-stacked-py

matplotlib은 개인적으로 간단한 선 그래프 정도를 표현할 때 유용할 것 같다. 꼭 matplotlib을 직접 사용하지 않더라도 pandas나 sympy등의 라이브러리를 사용해도 간단한 차트나 그래프를 그릴 수 있다. 물론 이들 라이브러리들도 플로팅 기능을 위한 백엔드로 matplotlib을 사용하고 있다.

다른 대안들

matplotlib으로 간단한 차트나 그래프를 그리는 것은 쉬운 일이지만 이걸 예쁘게 다듬고 커스터마이징 하기 위해서는 적지 않은 노력이 필요하다. 그래서 다른 대안들을 생각해 볼 필요도 있을 것이다. 먼저 ggplot2 가 있다. R에서 사용하는 시각화 라이브러리인데, 이것을 파이썬으로 포팅한 ggplot이 있다. ggplot2는 Grammar of Graphics라는 (사실은 이 개념을 소개한 책 제목이기도 하다.) 개념을 구현한 라이브러리이다. 우선 시각화하려는 데이터로부터 필요한 요소들을 정의하고, 여기에 적용할 시각적인 요소들을 결합하고 쌓아올려 최종적인 결과물을 만드는 방식을 채택하고 있다.

따라서 세부적인 요소들을 하나하나 손으로 다듬고 하나씩 그려서 그래프를 만들어나가는 MATLAB의 방식과는 차이가 있으며, (R 사용자들이 다들 좋아하는 이유가 있음) 결론적으로 코드도 매우 깔끔해진다.

다만 이를 포팅한 ggplot 라이브러리는 몇 년째 업데이트가 되지 않으며 pandas를 0.19버전으로 유지해야 작동하는 문제가 있다. 대신 plotnine 이라는 패키지로 다른 개발자가 다시 포팅한 것이 있으며, 이쪽은 원활하게 작동된다.

plotnine 역시 백엔드로는 matplotlib을 채택하여 그림을 그린다.

from plotnine import ggplot, aes, geom_density
from plotnine.data import mpg

ggplot(mpg, aes(x='cty', color='drv', fill='drv')) + geom_density(alpha=.1)


matplotlib을 그대로 사용하면서 pandas 데이터를 사용하는 경우라면 seaborn도 좋은 선택이다. seaborn은 matplotlib을 백엔드로 사용하면서 타입별 차트를 그리는 인터페이스를 단일 함수에서 사용한다. 색상 팔레트를 비롯해서 여러 디폴트 설정 값들이 매우 보기 좋은 결과물을 만든다.

다음은 차량의 연비와 차종의 관계를 산포도로 나타낸 것이다. seaborn은 Altair와 비슷하게 데이터프레임의 칼럼을 기반으로 자동으로 데이터를 추출하고 그래프를 그린다.

import seaborn as sns

sns.set(style='white')
# 기본적으로 예제용 데이터를 몇 가지 제공한다.
mpg = sns.load_dataset('mpg')
sns.relplot(x='horsepower', y='mpg', hue='origin', size='acceleration',
            sizes=(40, 220), alpha=.7, palette='muted',
            height=6, data=mpg)
plt.show()

만약 주피터 노트북등 웹 환경을 주로 사용한다면 Altair를 고려해 볼 필요도 있다. 이 라이브러리 역시 Grammar of Graphics를 채택한다. 사실 순수하게 파이썬으로 처음부터 끝까지 만들어진 것은 아니고, 웹용 시각화 라이브러리인 VEGA/VEGA Lite를 위한 데이터 포맷을 생성하며, 실제 아웃풋은 자바스크립트를 통해서 렌더링한다. Pandas 데이터프레임으로부터 다양한, 그리고 예쁜 차트를 정말 손쉽게 만들어 낼 수 있다. 다만 벡터 방식의 결과물을 생성하기 때문에 데이터 개수가 500개로 한정되는 단점이 있다. (내부적으로 개선한다 하더라도 SVG를 통해서 동적으로 차트를 그리기 때문에 프론트 엔드 성능이 딸리는 느낌이다.)


마지막으로 Plotly가 있다. Plotly는 Altair와 마찬가지로 자바스크립트를 사용해서 웹브라우저 상에서 이미지를 렌더링하는 라이브러리이다. D3.js를 사용해서 시각화하며 최근에 나온 라이브러리인만큼 굉장히 예쁜 결과물을 만들어준다. (게다가 마우스오버하면 툴팁도 막 띄워준다.)

https://plotly.com/python/

import plotly.express as px
df = px.data.stocks()
df2 = pd.melt(id_vars='date', var_name='company', value_name='price')
fig = px.line(df2, x='date', y='price', color='company')
fig.show()

소개한 것들 중에서 개인적으로 마음에 드는 것은 Altair이다. 문서화도 잘되어 있고 선언적인 인터페이스도 마음에 든다. 아직까지 파이차트를 지원하지 않는 부분은 좀 슬프지만…. 신생 라이브러리이니 발전할 가능성은 많다고 본다.

기회가 된다면 이러한 라이브러리들을 사용해서 데이터를 시각화하는 방법에 대해서 유형별로 좀 더 소개하도록 하겠다. 그럼 오늘은 이만…