본문 바로가기
데이터 분석/시각화

[Matplotlib] 등고선도(Contour Plot)을 그려보자 (feat. contour, contourf)

by 부자 꽁냥이 2023. 1. 10.

반갑습니다~ 꽁냥이입니다. 등고선도 또는 등치선도(Contour Plot)는 2차원 좌표계에서 같은 높이를 가지는 부분을 선으로 이어서 그린 것을 말합니다. 등고선도를 이용하면 좌표상의 높이(또는 z값)의 분포를 직관적으로 알 수 있는데요. 

 

이번 포스팅에서는 Matplotlib을 이용하여 등고선도를 그리는 방법에 대해서 알아보겠습니다. 여기서는 격자형 데이터를 이용하여 등고선도를 그리는 기본 방법을 알아보고 좌우 상하 간격이 일정하지 않은 데이터(Irregular Data)에 대한 등고선도 그리고 외삽(Extrapolation)을 이용하여 데이터 바깥 영역까지 등고선도를 그릴 수 있는 방법을 알아보겠습니다.

 

- 목차 -

1. 격자형 데이터

2. 간격이 일정하지 않은 데이터

3. 외삽(Extrapolation)을 이용한 바깥 영역 채워넣기


   1. 격자형 데이터

먼저 등고선도를 그릴 격자형 데이터를 만들어보겠습니다.

 

import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['axes.unicode_minus'] = False

size = 100
x = np.linspace(-2, 2 , size)
y = np.linspace(-1.3, 1.3, size)

## 격자형 데이터
X, Y = np.meshgrid(x, y) ## xy 데이터
Z = X*np.exp(-X**2-Y**2) ## z 데이터(높이)

 

이제 등고선도를 그려보겠습니다. Matplotlib에서는 ax.contour로 등고선을 그릴 수 있고 ax.contourf로 색상이 채워지는 등고선도를 그릴 수 있습니다. 둘의 차이는 전자는 라인만 그려주고 후자는 사이의 색상을 보간법을 이용하여 채워줍니다. 아래 코드를 살펴보겠습니다.

 

fig = plt.figure(figsize=(12,8))
fig.set_facecolor('white')
ax = fig.add_subplot()
contour1 = ax.contour(X, Y, Z, levels=10, colors='k', linewidths=1, linestyles='--') ## 등고선
contour2 = ax.contourf(X, Y, Z, levels=256, cmap='jet')

ax.clabel(contour1, contour1.levels, inline=True) ## contour 라벨
fig.colorbar(contour2, shrink=0.5) ## 컬러바 크기 축소 shrink

plt.show()

 

line 4~5

contour와 contourf 모두 격자 데이터 X, Y에 대한 데이터와 높이를 나타내는 Z가 차례대로 들어갑니다. 이때 Z는 반드시 2d array여야 합니다. 여기서 X, Y대신 1차원 배열인 x, y를 넣어도 동일하게 동작합니다. 그리고 contour에서는 levels를 통해 등고선 개수를 설정합니다. contourf에서는 levels를 통해 구분 색상 개수를 설정할 수 있으며 값이 커질수록 부드러운 색상 효과를 줄 수 있습니다.

 

line 7~8

clabel을 통해 등고선 값을 텍스트로 넣을 수 있습니다. 이때 첫 번째 인자는 contour의 결과 matplotlib.contour.QuadContourSet 객체, 두 번째 인자는 level을 지정합니다. 그리고 inline을 True로 설정하여 값이 등고선 내부에 들어가도록 해주었습니다(line 7). 그리고 fig.colorbar를 통해 컬러바를 추가했습니다(line 8).

 

위 코드를 실행하면 등고선도가 예쁘게 잘 그려진 것을 알 수 있습니다.


   2. 간격이 일정하지 않은 데이터

현실 세계에서는 격자형 데이터보다는 좌우 상하 간격이 일정하지 않은 경우(Irregular Data)가 대부분입니다. 이 경우에는 주어진 데이터를 이용하여 격자형 데이터를 만들어준 다음 등고선도를 그릴 수 있습니다. 

 

먼저 데이터를 만들어 봅시다.

 

np.random.seed(100)
real_sample_size = 50
x = np.random.uniform(-2,2 , real_sample_size)
y = np.random.uniform(-1.3, 1.3, real_sample_size)
z = x*np.exp(-x**2-y**2)

 

x, y 데이터만 그려보면 좌우 상하 간격이 일정하지 않은 것을 알 수 있습니다.

 

fig = plt.figure(figsize=(12,8))
fig.set_facecolor('white')
ax = fig.add_subplot()
ax.scatter(x, y, color='k', alpha=0.5, s=5)

plt.show()

 

일정하지 않은 데이터

앞에서 이러한 데이터를 격자형 데이터로 만들어줘야 한다고 했는데요. 이는 scipy 모듈에서 제공하는 griddata를 이용하여 만들 수 있습니다. 간단하게 원리를 말씀드리면 주어진 xy 데이터에 대해서 x, y 좌표의 최대 최소값을 이용한 격자 배열을 각각 만들어주고 해당 데이터를 이용하여 XY격자 데이터 행렬(2차원 배열)을 만듭니다. griddata는 원래 주어진, x, y, z를 이용하여 XY 격자 데이터에 대응하는 Z 데이터를 보간법(Interpolation)을 이용하여 추정하게 됩니다. 꽁냥이는 보간 방법으로 'cubic'을 주로 쓰고 있어요. griddata에 대한 자세한 사용법은 여기를 참고하시면 됩니다.

 

from scipy.interpolate import griddata

## 관측된 데이터를 이용하여 격자 데이터 생성
grid_size = 100
x_range = np.linspace(np.max(x), np.min(x), grid_size) ## x 범위
y_range = np.linspace(np.max(y), np.min(y), grid_size) ## y 범위
X, Y = np.meshgrid(x_range, y_range) ## XY 격자 데이터
Z = griddata(np.c_[x, y], z, (X, Y), method='cubic') ## Z 격자 데이터

 

이렇게 X, Y, Z를 만들어 준 다음 앞에서 배운 대로 등고선도를 그리면 되지요.

 

fig = plt.figure(figsize=(12,8))
fig.set_facecolor('white')
ax = fig.add_subplot()
contour1 = ax.contour(X, Y, Z, levels=10, colors='k', linewidths=1, linestyles='--') ## 등고선
contour2 = ax.contourf(X, Y, Z, levels=256, cmap='jet')

ax.clabel(contour1, contour1.levels, inline=True) ## contour 라벨
fig.colorbar(contour2, shrink=0.5) ## 컬러바 크기 축소 shrink

ax.scatter(x, y, color='k', alpha=0.5, s=5) ## 산점도 추가

plt.show()

 

보시는 바와 같이 등고선도가 그려졌습니다. 하지만 테두리 영역에 빈 공간이 보입니다. 아래에서 이러한 공간을 채우는 방법에 대해서 알아보겠습니다.


   3. 외삽(Extrapolation)을 이용한 바깥 영역 채워 넣기

앞에서 살펴보았듯이 griddata는 실제 관측치 내부 영역(Convex Hull 이라고도 합니다)을 벗어나는 부분은 NaN로 채워지며 이 부분은 등고선도를 그릴 때 빈 영역이 됩니다. 이것만으로도 만족할 수 있겠지만 사람의 욕심은 끝이 없는 법!! 바깥 영역도 외삽(Extrapolation)을 이용하여 상식적으로 그려보도록 하겠습니다.

 

데이터는 이전과 같습니다. 그리고 griddata를 이용하는 것도 이전과 같습니다.

 

np.random.seed(100)
real_sample_size = 50
x = np.random.uniform(-2,2 , real_sample_size)
y = np.random.uniform(-1.3, 1.3, real_sample_size)
z = x*np.exp(-x**2-y**2)

grid_size = 100
x_range = np.linspace(np.max(x), np.min(x), grid_size)
y_range = np.linspace(np.max(y), np.min(y), grid_size)
points = np.c_[x, y]
X, Y = np.meshgrid(x_range, y_range)
Z = griddata(points, z, (X, Y), method='cubic')

 

이제 외삽(Extrapolation)을 이용하여 바깥 부분의 값을 채워 넣어보겠습니다. 꽁냥이는 바깥 부분을 K-NN 방법을 이용하여 채워 넣었어요. K-NN을 이용하여 Z에서 결측이 없는 부분으로 예측 모형을 만들고 Z에서 결측이 있는 부분을 이에 대응하는 XY 격자 데이터를 이용하여 예측하는 방식으로 채워 넣는 것이지요.

 

from sklearn.neighbors import KNeighborsRegressor

reg = KNeighborsRegressor(n_neighbors=5, weights='distance').fit(np.c_[X[Z==Z], Y[Z==Z]], Z[Z==Z])
fill_z = reg.predict(np.c_[X[Z!=Z], Y[Z!=Z]])
Z[Z!=Z] = fill_z

 

자 이제 등고선도를 그려보겠습니다.

 

fig = plt.figure(figsize=(12,8))
fig.set_facecolor('white')
ax = fig.add_subplot()
contour1 = ax.contour(X, Y, Z, levels=10, colors='k', linewidths=1, linestyles='--') ## 등고선
contour2 = ax.contourf(X, Y, Z, levels=256, cmap='jet')

ax.clabel(contour1, contour1.levels, inline=True) ## contour 라벨
fig.colorbar(contour2, shrink=0.5) ## 컬러바 크기 축소 shrink

ax.scatter(x, y, color='k', alpha=0.5, s=10)

plt.show()

 

 

바깥 영역이 나름 합리적으로(?) 그려진 것을 알 수 있습니다.


등고선도는 실제로 데이터 분석에서 높이 또는 Z 축 데이터에 대한 분포를 XY 평면에서 직관적으로 볼 수 있어서 많이 사용됩니다. 오늘 배운 내용을 숙지하시면 매우 유용하게 많은 곳에서 사용할 수 있을 거라 확신합니다. 다음에도 좋은 주제로 찾아뵐 것을 약속드리며 이상 포스팅 마치겠습니다. 지금까지 꽁냥이의 글 읽어주셔서 감사합니다.


댓글


맨 위로