본문 바로가기

Python

[Python] 그래프의 특정 부분을 확대하여 PIP로 표현하기.

반응형

위 그래프는 $ \frac{dy}{dt} = y-t^2+1 $ 이라는 ODE를 $t=0$ 에서 $t=2.0$ 까지 해석적인 방법 (파란색) 과 다양한 수치적인 방법들 (Euler method ~ RK4 method) 을 이용해서 풀어낸 것이다. Euler method와 나머지 방법은 t가 증가함에 따라 꽤 오차가 많이 나기 때문에 쉽게 구분가능하지만, 나머지 방법들은 꽤나 정확해서 어떤게 오차가 더 큰지 한 눈에 알아보기 어렵다.

위 경우에서 궁금한 부분을 확대해서 볼 수 있는 트릭이 여러가지가 있었다. 그 중 나는 mpl_toolkits에서 제공하고 있는 zoomed_inset_axes를 사용했다. 내가 원하던 방식이 딱 이것(Picture in picture방식)이었고, 제일 깔끔했기 때문이다. 

 

나는 코랩에서 작업 했는데, matplotlib을 3.4.1 버젼을 사용했다. ( 2021년 9월 13일 기준으로 3.4.3이 최신버젼이다.) 

코랩의 경우 matplotlib이 구형버젼이기 때문에, 최신 버젼을 사용해주려면 따로 설치를 해주어야 한다. 

!pip install matplotlib==3.4.1

굳이 최신버젼을 깔아주는 이유는, 버전이 올라가면서 사용성이 더 높아지는 측면도 있고, 공식 문서를 찾아보기 편하기 때문이다. (나는 대부분의 경우 먼저 가이드 문서를 참고하고, 내 상황에 맞게끔 변형한다.)

설치를 한 이후에는, 런타임 다시시작을 꼭 해주고, 필요한 모듈들을 import해준다. 이번 그래프 확대에서 꼭 써야하는 모듈은 다음과 같다. 

import matplotlib as mpl
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset

참고로 Zoomed_inset_axes를 활용하는 레퍼런스 문서는 다음과 같다. https://matplotlib.org/stable/api/_as_gen/mpl_toolkits.axes_grid1.inset_locator.zoomed_inset_axes.html#mpl_toolkits.axes_grid1.inset_locator.zoomed_inset_axes

위의 ODE를 풀어내기 위해서, 또 그래프를 예쁘게 그려주기 위해서 추가로 사용한 모듈은 아래와 같다. 

import numpy as np 
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import seaborn as sns

위의 그래프를 그리기 위해서, ODE를 각각 다른 방식으로 전부 계산해주었는데, 이 과정은 생략하겠다. (나중에 수치적으로 푸는 방법을 따로따로 게시글로 업로드해보겠다.) 

 

일단 그래프를 그린 전체 코드를 한 번 보자.

#initializing figure state
fig, ax3 = plt.subplots(figsize=[6,6])
sns.set_palette("bright") 

#Main figure plotting
ax3.plot(np.arange(0,2,0.0001),values,label = 'Analytic')
ax3.plot(Euler_tlist,Euler_ylist,label = 'Euler')
ax3.plot(RK2_tlist,RK2_ylist,label = 'RK2',linestyle=':')
ax3.plot(ModE_tlist,ModE_ylist,label = 'Mod. Euler',linestyle=(0,(1,1)))
ax3.plot(Heun_tlist,Heun_ylist,label = 'Heun',linestyle='--')
ax3.plot(RK4_tlist,RK4_ylist,label = 'RK4',linestyle='dashdot')

#Zoomed part
axins = zoomed_inset_axes(ax3, 40, loc='lower right', axes_kwargs={"facecolor" : "lightgray"}) # PIP, zoom = 4

#plot
axins.plot(np.arange(0,2,0.0001),values,label = 'Analytic')
axins.plot(Euler_tlist,Euler_ylist,label = 'Euler')
axins.plot(RK2_tlist,RK2_ylist,label = 'RK2',linestyle=':')
axins.plot(ModE_tlist,ModE_ylist,label = 'Mod. Euler',linestyle=(0,(1,1)))
axins.plot(Heun_tlist,Heun_ylist,label = 'Heun',linestyle='--')
axins.plot(RK4_tlist,RK4_ylist,label = 'RK4',linestyle='dashdot')
axins.plot(Hyeok_tlist,Hyeok_ylist,label = 'Hyeok',linestyle='dotted')
x1, x2, y1, y2 = 1.98,2.0,5.25,5.3
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)

#set xyticks invisible 
axins.set_xticks([])
axins.set_yticks([])
axins.grid()
mark_inset(ax3, axins, loc1=2, loc2=4, fc="lightgray", ec="0.5")

#Ax-Porperties
#Major and minor xtics
ax3.xaxis.set_major_locator(ticker.MultipleLocator(0.5))
ax3.yaxis.set_major_locator(ticker.MultipleLocator(1))
ax3.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
ax3.yaxis.set_minor_locator(ticker.MultipleLocator(0.5))

#label coord
ax3.yaxis.set_label_coords(-0.125,0.445)
ax3.xaxis.set_label_coords(0.48,-0.1)

#set x label and y 
ax3.set_xlabel('t', fontsize = 12, fontweight='bold')
ax3.set_ylabel('y',rotation=0, fontsize = 12, fontweight='bold')
ax3.set_xlim(0,2.0)
ax3.set_ylim(0,5.5)

#Axes font board
plt.setp(ax3.get_xticklabels(), fontsize =10,fontweight='bold')
plt.setp(ax3.get_yticklabels(), fontsize =10,fontweight='bold')
plt.setp(ax3.get_xticklabels(), fontsize =10,fontweight='bold')
plt.setp(ax3.get_yticklabels(), fontsize =10,fontweight='bold')

#Border
mpl.rcParams['axes.linewidth'] = 2

#legend
legend_properties = {'weight':'bold','size':'18'}
ax3.legend(loc='upper left',frameon=False, prop=legend_properties)

plt.draw()
plt.savefig("Finalone", dpi=300, bbox_inches = "tight")
plt.show()

어질어질해보이지만, 실상 Zoom하는 부분은 몇 줄 되지않는다. 살펴보자.

 

#initializing figure state
fig, ax3 = plt.subplots(figsize=[6,6])
sns.set_palette("bright") 

#Main figure plotting
ax3.plot(np.arange(0,2,0.0001),values,label = 'Analytic')
ax3.plot(Euler_tlist,Euler_ylist,label = 'Euler')
ax3.plot(RK2_tlist,RK2_ylist,label = 'RK2',linestyle=':')
ax3.plot(ModE_tlist,ModE_ylist,label = 'Mod. Euler',linestyle=(0,(1,1)))
ax3.plot(Heun_tlist,Heun_ylist,label = 'Heun',linestyle='--')
ax3.plot(RK4_tlist,RK4_ylist,label = 'RK4',linestyle='dashdot')

먼저 맨 위의 주석부분은 fig라는 도화지에 ax3축을 subplots로 6,6사이즈로 잡아주었다. 그리고 seaborn의 bright 팔레트를 사용해주었다.  

seaborn의 bright palette는 위와 같이 생겼다.

다음으로 ax3에 각 methods의 t값과 y값을 넣고, label과 라인스타일을 정해주었다. 여기까지는 문제 없었으리라 생각한다. 

 

여기서부터가 핵심 내용이다.

#Zoomed part (PIP), zoom = 40
axins = zoomed_inset_axes(ax3, 40, loc='lower right', axes_kwargs={"facecolor" : "lightgray"})

1. 먼저 axins라는 변수에 zoomed_inset_axes를 설정해준다. 이 zoomed_inset_axes가 바로 Zoom을 해주는 부분이다.

2. 이 친구는 먼저 parent_axes를 설정해주어야 한다. 우리의 경우 ax3 하나 밖에 없으므로 ax3을 넣어주었다.

3. 다음으로는 얼마나 Zoom할건지를 넣어주어야한다. 나는 40배로 넣어주었는데, 각자 상황에 맞게, 취향에 맞게 Zoom 할 부분의 크기를 결정해주면 되겠다. 감이 안오면 plt.show() 해보면서 찾을 수 밖에 없다. float이 들어가지기 때문에 소수를 넣으면 되고, 1보다 작은 수를 넣으면 Zoom이 아니고 그래프를 축소하는 효과를 볼 수 있다. 

4. loc에는 Zoom할 위치를 어디에 둘 건지를 알려주면 되는데, 가능한 위치는 아래와 같다. 

'upper right' : 1 'center left' : 6
'upper left' : 2
'center right' : 7
'lower left' : 3
'lower center' : 8
'lower right' : 4
'upper center' : 9
'right' : 5
'center' : 10

숫자로 넣어도 되고, 타이핑 해서 넣어줘도 상관없다. 

5. axes_kwargs에는 기타 여러 옵션을 줄 수 있는데, 나는 facecolor를 lightgray로 설정해줌으로써, 조금 더 강조되게끔 그려주었다. 

 

자 이제 axins는 설정이 전부 끝났고, 이 안에 그래프를 그려주어야한다. 

#plot
axins.plot(np.arange(0,2,0.0001),values,label = 'Analytic')
axins.plot(Euler_tlist,Euler_ylist,label = 'Euler')
axins.plot(RK2_tlist,RK2_ylist,label = 'RK2',linestyle=':')
axins.plot(ModE_tlist,ModE_ylist,label = 'Mod. Euler',linestyle=(0,(1,1)))
axins.plot(Heun_tlist,Heun_ylist,label = 'Heun',linestyle='--')
axins.plot(RK4_tlist,RK4_ylist,label = 'RK4',linestyle='dashdot')
axins.plot(Hyeok_tlist,Hyeok_ylist,label = 'Hyeok',linestyle='dotted')

아까 ax3에 그려준 그래프를 몽땅 axins에도 그려주면 된다. (새로운 axes가 생겼다고 봐도 무방하다.) 

x1, x2, y1, y2 = 1.98,2.0,5.25,5.3
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)

그 다음, 내가 Zoom 하고 싶은 구간만 정확하게 잘라서 (!) 리밋을 걸어줌으로써 마무리 짓는다. 이제 axins라는 축에는 위에서 리밋 걸어준 것 처럼 t=1.98 ~t=2.0, y=5.25~y=5.3 부분만 보일 것이다. 이제 정말 다 왔다.

#set xyticks invisible 
axins.set_xticks([])
axins.set_yticks([])
axins.grid()

마지막으로 axins의 xticks와 yticks를 숨겨주고, grid도 표현해줌으로써 그래프 그리기가 끝났다. 그런데 아래와 같이 뭔가 허전한 그림이 나오게 된다. 

그렇다. 어디서부터 어디까지 Zoom을 한건지 표시가 되어있지 않아서, 그래프를 보는 사람들이 오른쪽 아래 그래프가 뭔지 이해를 못할 수 있다. 어디서부터 어디까지 Zoom을 했는지 선을 그어주는 작업이 필요한데, 딱 코드 한 줄로 해결할 수 있다.

mark_inset(ax3, axins, loc1=2, loc2=4, fc="lightgray", ec="0.5")

mark_inset 인데, 1. 똑같이 먼저 parent를 지정해주고, 2. 그 다음 연결할 그래프를 입력해준다. 우리의 경우 axins이다. 

3. loc1과 loc2에는 선이 네모 박스의 어떤 코너에 연결될지를 결정한다. 2,4로 했을 경우와 1,3으로 했을 경우를 비교하면 아래와 같다. 


loc1=2, loc2=4

loc1=1, loc2=3

4. 다음 옵션인 fc는 선의 색깔을, 5. ec는 선의 두께를 각자 알아서 조절해주면 되겠다. 

 

그 밑으로 들어간 여러 옵션은 그래프의 나머지 옵션이니 설명을 생략하겠다.