극좌표계(polar coordinate system)는 평면 위의 위치를 각도와 거리 성분으로 표현하는 2차원 좌표계입니다. 일반적으로 사용하는 x/y축으로 표현되는 데카르트 좌표계에서는 표현하기 어려운 것들을 극좌표계로 표현하면 쉽게 표현되는 경우들이 있어서 종종 사용됩니다. [1] 예를 들어, 레이더 , 바람의 풍향과 풍속과 같은 것을 표현하는데 많이 사용했고, 최근에는 여러 요소들을 비교가 필요한 게임 / 모델 비교 등에도 활용이 가능합니다.
극좌표계는 시각화된 차트가 놓일 틀일 뿐, 실제로 그림을 넣을 수 있습니다. 일반적으로 사용하는 데카르트 좌표계가 아니다보니, 심미적으로 괜찮은 시각화가 될 수 있지만 때로는 명확한 데이터 비교가 어려울 수 있으니 충분히 고민이 필요합니다.
# 설정
N = 6
r = np.random.rand(N)
theta = np.linspace(0, 2*np.pi, N, endpoint=False)
# 막대 그래프 그리기
fig = plt.figure()
ax = fig.add_subplot(111, projection='polar')
ax.bar(theta, r, width=0.5, alpha=0.5)
plt.show()
# 실습 3. 극좌표계에 선그래프 넣기
극좌표계는 다른 그래프인 선 그래프를 넣어보겠습니다. 아르키메데스 나선이라 불리는 비교적 간단한 나선을 입력하면 아래와 같이 그림이 나오게 됩니다. [2]
import matplotlib.pyplot as plt
import numpy as np
r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
ax.plot(theta, r)
ax.set_rmax(2)
ax.set_rticks([0.5, 1, 1.5, 2]) # Less radial ticks
ax.set_rlabel_position(-22.5) # Move radial labels away from plotted line
ax.grid(True)
ax.set_title("A line plot on a polar axis", va='bottom')
plt.show()
# 실습 4. 축구 선수 능력치 시각화
레이더 차트(Radar chart)는 극좌표계에 값을 채워가는 방식으로 그래프를 그리는 것입니다. 실제로 게임에서 많이 활용되는 차트 형태로 데이터를 한번에 보기에 좋은 차트 입니다. 대표적으로 사용하는 게임인 축구 게임으로 레이더 차트를 시각화를 해보겠습니다. 아래 그림은 축구 게임의 손흥민 선수의 능력치입니다. 해당 값을 바탕으로 레이더 차트를 만들어 보겠습니다.
FC 24 손흥민 선수 능력치 [3]
import matplotlib.pyplot as plt
import numpy as np
stats = ["PAC", "SHO", "PAS", "DRI", "DEF", "PHY"]
theta = np.linspace(0, 2*np.pi, 6, endpoint=False)
son = np.array([87, 88, 80, 84, 42, 70])
# 끝의 점끼리 연결
son = son.tolist() + [son[0]]
theta = theta.tolist() + [theta[0]]
fig = plt.figure()
ax = fig.add_subplot(111, projection='polar')
ax.plot(theta, son, color='forestgreen')
ax.fill(theta, son, alpha=0.3, color='forestgreen')
ax.set_thetagrids([n*60 for n in range(6)], stats)
ax.set_rmax(100)
plt.show()
시각화를 진행하다보면 데이터로 그려진 그래프만으로는 부족한 경우가 있습니다. 아래와 같은 사례가 있을 수 있죠.
특정 값 (최대, 최소 등)에 대한 지칭이 필요하다.
어느 한점으로부터 얼마나 멀리 떨어져 있는지 시각적으로 바로 알아차리게 하고 싶다.
특정한 구간을 표시하고 싶다.
이번 글에서는 시각화 효과를 높이기 위해 다양한 도구들에 대해 살펴보려고 합니다.
다양한 그리드 생성하기
matplotlib에서는 pyplot.grid를 통해 그리드를 생성할 수 있습니다. 하지만, 그리드의 사전적 의미처럼 격자 형태의 비교적 단순한 형태의 그리드만 생성할 수 있어 유연성에 있어서는 조금 아쉽습니다. 대신에 matplotlib에서 제공하고 있는 다양한 차트 시각화 도구를 이용해 색상, 선의 유형, 두께 등을 조절해서 조금 덜 보이도록 해서 다양한 그리드를 생성할 수 있습니다. 여기선 직선과 원 형태의 그리드의 간단한 형태를 생성해보도록 하겠습니다.
먼저, 아래와 같이 [0,1] 사이에 10개의 랜덤 데이터를 생성하겠습니다. 이 데이터를 바탕으로 다양한 보조선을 그려보도록 하겠습니다.
import numpy as np
import matplotlib.pyplot as plt
x = np.random.uniform(0, 1, size=10)
y = np.random.uniform(0, 1, size=10)
np.random.seed(1)
# 실습 1. x + y = c 그리기
x + y = c (c는 바뀜)직선을 그리는 방법은 1차 함수의 x, y 절편을 변화시키면서 그 값을 변화시켜주면 됩니다. 주된 그래프가 아니기 때문에 잘 보이지 않게 점선(--), 회색, 투명도를 설정하고 그려줄 수 있습니다.
fig, ax = plt.subplots()
ax.scatter(x, y)
# Grid : x + y = c
x_start = np.linspace(0, 2.2, 12, endpoint=True) # 절편 값 변화에 따라 나눔
for xs in x_start:
ax.plot([xs, 0], [0, xs], linestyle='--', color='gray', alpha=0.5, linewidth=1)
ax.set_title(r"Grid ($x+y=c$)", fontsize=15,va= 'center', fontweight='semibold')
ax.set_xlim(0, 1.1)
ax.set_ylim(0, 1.1)
plt.show()
# 실습 2. y = cx 그리기
y = cx (c는 바뀜) 직선은 위와 다르게 기울기가 변화하는 직선입니다. 이를 그리는 방법은 기울기의 변화를 원하는 방식으로 변화를 주면서 직선을 그려주면 됩니다. 메인 그래프가 아니라 그리드이기 때문에 잘 보이지 않게 점선(--), 회색, 투명도를 설정하고 그려줄 수 있습니다.
특정 점에서 얼마나 떨어져 있는지 살펴보기 위해선 원형 그리드가 효과적입니다. 실제로 (유클리드 공간에서) 원의 정의가 같은 거리에 있는 점들의 집합이기 때문에 정확한 활용이죠. 여기선 x[2], y[2] 값을 중심으로 얼마나 떨어져 있는지 살펴보는 코드는 아래와 같습니다. (그림을 보면 조금 이상하긴 하지만... 조금씩만 조정해주면 될 것 같습니다)
fig, ax= plt.subplots()
ax.scatter(x, y)
## Grid : (x-a)**2 + (y-b)**2 = r**2
a = x[2]
b = y[2]
rs = np.linspace(0.1, 0.8, 8, endpoint=True)
for r in rs:
xx = r*np.cos(np.linspace(0, 2*np.pi, 100))
yy = r*np.sin(np.linspace(0, 2*np.pi, 100))
ax.plot(xx+a, yy+b, linestyle='--', color='gray', alpha=0.5, linewidth=1)
ax.text(a+r*np.cos(np.pi/4), b-r*np.sin(np.pi/4), f'{r:.1}', color='gray')
ax.set_title(r"Grid ($(x-a)^2+(y-b)^2=c$)", fontsize=15,va= 'center', fontweight='semibold')
ax.set_xlim(0, 1.1)
ax.set_ylim(0, 1.1)
plt.show()
왼쪽으로 길어서 찌그러져 있어 보이지만.. 원(에 가까운 다각형) 맞습니다.
보조 선/면 그리기
시각화는 데이터를 보기 좋게 만드는 것입니다. 앞서 그리드 외에도 데이터를 살펴보기 좋게끔 만들기 위해 보조 선/면을 그리는 방법이 있습니다. 아래 그림은 kaggle에서 볼 수 있는 시각화 사례입니다.
면적을 활용해 효과적인 시각화 사례 : 연령대별 넷플릭스 평가 분포 [1]
pyplot에서 보조선(axhline, axvline)과 보조면(axhspan, axvspan)을 그릴 수 있는 다양한 메서드를 제공하고 있습니다.
# 실습 4. 보조 선 그리기
보조 선을 그리는 방법은 앞에서 설명한 메서드(axvline, axhline)을 사용하거나 pyplot.plot 을 사용하는 방법이 있습니다. 정답은 없고, 둘 중 편한 것을 사용하면 될 것 같습니다. 아래 코드는 두가지 모두 사용한 방법을 기재했습니다. 조금 주의할 것은 axvline, axhline에서 xmin과 xmax는 0~1 사이의 상대적인 위치를 넣어야 하기 때문에 표화가 필요합니다.
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
x = np.arange(20)
y = np.random.rand(20)
ax = fig.add_subplot(111)
ax.plot(x, y,
color='lightgray',
linewidth=2,)
ax.set_xlim(-1, 21)
# max
# ax.plot([-1, x[np.argmax(y)]], [np.max(y)]*2,
# linestyle='--', color='tomato')
ax.axhline(y=np.max(y), xmin=0, xmax= x[np.argmax(y)] / len(x),
linestyle='--', color='tomato')
ax.scatter(x[np.argmax(y)], np.max(y),
c='tomato',s=50, zorder=20)
# min
# ax.plot([-1, x[np.argmin(y)]], [np.min(y)]*2,
# linestyle='--', color='royalblue')
ax.axhline(y=np.min(y), xmin=0, xmax= x[np.argmin(y)] / len(x),
linestyle='--', color='royalblue')
ax.scatter(x[np.argmin(y)], np.min(y),
c='royalblue',s=50, zorder=20)
plt.show()
# 실습 5. 보조 면 그리기
보조 면의 사용은 axvspan, axhspan을 통해 사용이 가능합니다. 사용방법은 위에 보조선과 거의 유사합니다.
직사각형 막대를 이용해 데이터 값을 표현하는 대표적인 차트입니다. 범주에 따른 수치 값을 비교할 때 적합한 방법으로 많이 사용합니다. mataplotlib.pyplot에서는 .bar (일반적인 수직형 막대) / .barh (수평형 막대, 범주가 많을 때 아래로 내리면서 사용) 크게 두가지 방법으로 사용할 수 있습니다.
먼제 seaborn에 내장되어 있는 titanic 데이터를 불러와 데이터를 준비하겠습니다.
# Library
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
titanic = sns.load_dataset('titanic')
print(titanic.head())
여러 개의 그룹을 쌓아서 표시하는 누적 막대 그래프는 한꺼번에 다양한 카테고리를 살펴볼 수 있는 장점이 있습니다. 위에서 생성한 타이타닉 데이터셋에 bar로 먼저 바닥에 만들 그래프를 생성하고, 그 위에 bottom 파라미터에 바닥에 있을 데이터를 지정해 얹는 방식으로 진행합니다. 만약 barh를 사용하는 경우 lef 파라미터를 사용합니다.
전체 비율을 나타내기 위해서는 100 % 기준 누적 막대 그래프(Percentage stacked bar chart)를 활용해주는 것도 좋습니다. 전체 비율을 계산하기 위한 total 값 계산만 추가해서 만들어주면 됩니다.
# Percentage Stacked Barplot
fig, ax = plt.subplots()
group = group.sort_index(ascending=False)
total=group['male']+group['female']
ax.barh(group['male'].index, group['male']/total,
color='royalblue')
ax.barh(group['female'].index, group['female']/total,
left=group['male']/total,
color='tomato')
ax.set_xlim(0, 1)
for s in ['top', 'bottom', 'left', 'right']:
ax.spines[s].set_visible(False)
plt.show()
실습 4. 묶은 세로 막대형 (Grouped bar plot)
matplotlib으로 구현이 쉽지는 않지만, 여러 유형의 카테고리를 묶어서 같이 표현하는 방법도 가능합니다. 너비만큼 x축으로 평행이동 시키면서 막대 그래프를 지속적으로 그려주는 형태로 그려줄 수 있습니다. 아래는 matplolib에서 소개하고 있는 예시입니다. [1]
# data from https://allisonhorst.github.io/palmerpenguins/
import matplotlib.pyplot as plt
import numpy as np
species = ("Adelie", "Chinstrap", "Gentoo")
penguin_means = {
'Bill Depth': (18.35, 18.43, 14.98),
'Bill Length': (38.79, 48.83, 47.50),
'Flipper Length': (189.95, 195.82, 217.19),
}
x = np.arange(len(species)) # the label locations
width = 0.25 # the width of the bars
multiplier = 0
fig, ax = plt.subplots(layout='constrained')
for attribute, measurement in penguin_means.items():
offset = width * multiplier
rects = ax.bar(x + offset, # x축 평행 이동
measurement,
width,
label=attribute) # 레이블 지정
ax.bar_label(rects, padding=3)
multiplier += 1
# Add some text for labels, title and custom x-axis tick labels, etc.
ax.set_ylabel('Length (mm)')
ax.set_title('Penguin attributes by species')
ax.set_xticks(x + width, species)
ax.legend(loc='upper left', ncols=3)
ax.set_ylim(0, 250)
plt.show()
산점도는 좌표계 위에 점들을 표시하여 변수 간 관계를 나타내는 방법입니다. matplotlib에서는 pyplot.scatter로 사용할 수 있으며, scatter에서 데이터를 구분하기 위한 주요 시각화 요소로는 color(c), marker, size(s)가 있습니다. 실제로 적용할 수 있는 것들을 실습해보면서 연습해보겠습니다. 먼저 데이터 분석 공부할 때 많이 사용하는 붓꽃 데이터를 불러와서 준비하도록 하겠습니다.
위에서 준비한 데이터를 바탕으로 Sepal Length와 Sepal Width를 x,y 축으로 놓고, 종별로 색을 구분하고 Petal Width에 따라 사이즈를 구분해보겠습니다. 색은 label별, 사이즈는 petal width 값에 따라 변화하고, 겹치는 내용이 있을 수 있기 때문에 alpha 값을 통해 투명도를 조정했습니다.
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(111)
for species in iris['Species'].unique():
iris_sub = iris[iris['Species']==species]
ax.scatter(x = iris_sub['sepal length (cm)'],
y = iris_sub['sepal width (cm)'],
s = iris_sub['petal width (cm)'] * 100, # 크기x100 키우기
alpha = 0.5, # 투명도 조정
label = species)
ax.legend()
plt.show()
실습 2. 여러 scatter plot 한번에 그리기
산점도는 두가지 변수에 대한 상관관계를 시각적으로 보기에 유리한 차트이지만, 여러 변수가 존재하는 경우 하나의 차트에 놓고 살펴보는 것이 좋습니다. 아래 코드는 여러 차트를 한꺼번에 for문을 통해 생성하는 코드입니다.
fig, axes = plt.subplots(4, 4, figsize=(14, 14))
features = iris.columns[:-1] # label만 제외
for i, f1 in enumerate(features):
for j, f2 in enumerate(features):
if i <= j :
axes[i][j].set_visible(False) # 중복 제거
continue
for species in iris['Species'].unique():
iris_sub = iris[iris['Species']==species]
axes[i][j].scatter(x=iris_sub[f2],
y=iris_sub[f1],
label=species,
alpha=0.7)
if i == 3: axes[i][j].set_xlabel(f2)
if j == 0: axes[i][j].set_ylabel(f1)
plt.tight_layout()
plt.show()
Matplotlib에서 선 그래프는 plot()을 통해 그릴 수 있습니다. plot()은 기본적으로 plot(x, y, fmt)으로 구성되며, fmt은 marker(데이터 표시), color(색), line(선 스타일)으로 구분됩니다. x, y는 별도 인자 선언없이 순서대로 넣어주면 되고, 나머지는 선언을 해주고 값을 입력하여 파라미터를 전달하는 과정이 필요합니다. 다만, plt를 통해 제공하고 있는 기능은 매우 다양하니, 필요할 때마다 찾아보는 습관이 필요합니다.
과학, 공학에서 x,y 축 외에 추가적인 축으로 설명이 필요한 경우가 있습니다. (예를 들어, 각도는 라디안과 ∘로 표기하고 그 값이 입력된 sin값 등) 이럴 경우 축을 추가할 수 있는데 ax 클래스에 secondary_xaxis, secondary_yaxis를 통해 추가 가능합니다. [1]
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(layout='constrained')
x = np.arange(0, 360, 1)
y = np.sin(2 * x * np.pi / 180)
ax.plot(x, y)
ax.set_xlabel('angle [degrees]')
ax.set_ylabel('signal')
ax.set_title('Sine wave')
def deg2rad(x):
return x * np.pi / 180
def rad2deg(x):
return x * 180 / np.pi
secax = ax.secondary_xaxis('top', functions=(deg2rad, rad2deg))
secax.set_xlabel('angle [rad]')
plt.show()
실습 3. 축 및 그래프 추가 (twinx, 보조축)
같은 x축을 공유하면서 맞은 다른 그래프를 추가하는 다른 방법도 존재합니다. twinx를 통해 x를 동일하게 공유하고, 같은데 ax에 추가하는 방법입니다. 아래와 같은 코드로 작성하면 됩니다만, 그래프를 추가할 경우 가독성이 떨어질 우려가 있으니 주의해서 사용할 필요가 있겠습니다.
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
np.random.seed(97)
x = np.arange(20)
y1 = np.random.rand(20)
y2 = np.random.rand(20)
# 첫번째 시각화
ax.plot(x, y1, color='blue')
ax.set_ylabel('y1')
# 두번째(보조축) 시각화
ax2 = ax.twinx()
ax2.plot(x, y2, color='tomato')
ax2.set_ylabel('y2')
plt.show()
이전에 전세계 발전사업현황에 대한 업데이트에 이어, 국내 발전사업허가 현황을 버블차트로 나타내고자 합니다. 앞서, 게시했던 글에서 문제점 중 하나가 colorbar를 활용해서 편하게 색깔을 표시했지만, 에너지원별로 색의 구분이 쉽지 않았는데요. 이번에는 색에 대한 지정(xkcd)을 통해 좀 더 명확하게 에너지원별로 구분이 될 수 있게끔 하고자 합니다.
우선, 중요한 버전은 다음과 같습니다. Basemap에 대한 설명과 설치에 버전의 영향을 좀 받기 때문에 관련한 내용은 앞서 언급한 전세계 발전사업현황 시각화 글을 참고(링크)하여 주시기 바랍니다.
목적은 전기위원회에서 제공하고 있는 발전사업허가 획득 프로젝트들의 현황을 에너지원, 용량, 위치에 대한 정보를 한꺼번에 보여줄 수 있는 시각화를 진행하는 것 입니다. 전반적인 흐름을 간략하게 표현하자면 다음과 같습니다.
Basemap을 이용해 한반도 그리기
데이터 확보 및 처리
버블차트 생성
1. Basemap을 이용해 한반도 그리기
우선, 목적한 시각화를 위해 밑그림이 필요합니다. Basemap에는 여러 기능을 제공하고 있는데, 'merc'를 이용해 그려보았습니다. 편의를 위해 draw_hanbando()란 함수를 만들어 Basemap 객체를 관리하도록 했습니다.
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import os
os.environ['PROJ_LIB'] = '가상환경 위치'
def draw_hanbando():
# 한반도 그려내기
plt.figure(figsize=(10,10))
m = Basemap(projection='merc', lat_0=37.35, lon_0=126.58, resolution = 'h',
urcrnrlat=40, llcrnrlat=33, llcrnrlon=122, urcrnrlon=132)
m.drawcoastlines()
m.drawcountries()
m.drawmapboundary()
# 위도, 경도를 설정해 한반도가 보이게끔
parallels = np.arange(30.,50.,1.)
m.drawparallels(parallels,labels=[True,False,False,False])
meridians = np.arange(120.,150.,2.)
m.drawmeridians(meridians,labels=[True,False,False,True])
return m
m = draw_hanbando()
m
위 코드를 실행하면 아래와 같이 그림이 잘 나오는 걸 확인할 수 있습니다.
2. 데이터 확보 및 처리
다음은 데이터를 확보하고 목적에 부합하게끔 적절하게 처리가 필요합니다. 다행스럽게도(?), 우리나라는 발전사업허가를 취득한 프로젝트들에 대해서 현황대장을 공개하고 있습니다. (링크)를 따라가면 '3MW 초과 발전사업 허가대장'이라는 제목의 공지글을 확인할 수 있는데, 매달 혹은 분기별로 업데이트를 진행하고 있습니다. 아쉬운 점은 불허가 데이터는 없다는 것인데, 이번 글의 목적이 발전사업허가 획득 현황을 보고자함이니 불허건들에 대해서는 다루지 않겠습니다.
위 게시글에 가면, pdf로 된 파일이 있는데 개인적으론 adobe의 힘을 빌어 엑셀로 변환해 사용했습니다. 그리고 데이터를 확인하면, 중복(변경 건) 및 표준화되지 않은 데이터들(용량, 에너지원)이 있는데, 중복은 제거하고 용량 및 에너지원은 약간 노가다로 표준화를 진행했습니다.
이렇게 전처리한 데이터셋을 이용해 14가지의 에너지원으로 분류하여 아래와 같이 정리했습니다. 참고로, 에너지원별로 색에 대한 구분을 확실하게 하기 위해 matplotlib의 color guide에서 이야기하는 색 레퍼런스 중 xkcd를 참조하여 작성했습니다.
여기서는 언급하지 않았지만, basemap으로 그림을 그리기 위해서는 (x, y) 형태의 좌표가 필요한데요. 아쉽게도, 발전사업허가대장에서는 정확한 좌표를 제공하고 있지 않습니다. 대신 제공하고 있는 주소가 있는데, 이를 이용해 geocoding이 필요합니다. 지오코딩을 위한 방법론은 구글API, ArcGIS 등 다양한게 있지만, 저는 구글을 이용했습니다.
3. 버블차트 생성
과정 1에서 만든 한반도 지도와 과정 2를 통해 얻은 데이터 및 색구분을 활용해 버블차트를 생성할 차례입니다. 위에서 에너지원별로 groupby로 객체를 에너지원별로 나눈 것을 좌표별로 x, y를 대입하고, 크기(s)는 용량/10 (현재 figure size에서 적절한 크기), 색(c)은 에너지원별로 할 수 있게끔 코드를 작성했습니다.
for name, group in groups:
try:
# 다른 프로젝트에서 활용한 데이터셋이다보니 여기서는 '불허여부' 존재
approval = group[group['불허여부']==0]
for i in approval.index:
x, y = m(approval['longitude'][i], approval['latitude'][i])
m.scatter(x, y, s=approval['용량'][i]/10, c=energy[name],alpha=0.5)
except:
continue
plt.show()
Single character shorthand notation for some basic colors. Note The colors green, cyan, magenta, and yellow do not coincide with X11/CSS4 colors. Their particular shades were chosen for better visibility of colored lines against typical backgrounds.
이전에 folium을 활용하여 데이터를 시각화한 적이 있습니다. 하지만, folium을 활용하면 html 형태로 산출물이 나오기 때문에 다른 프로젝트(웹 등) 적용에는 용이하나, 보고서와 같이 A4에 쓰기에는 이미지 캡처를 해야 하고, 필요한 정보 이상이 들어오기 때문에 마음이 안 들수도 있을 것 같습니다.
이를 위해 찾아본 결과 matplotlib의 third-party 개념의 라이브러리(mpl-toolkits.basemap)가 있는데, 이를 해결하기에 꽤 괜찮아보여 시도해봤고 개인적으론 만족스러운 결과를 얻을 수 있어 공유하고자 합니다. 즉, 1) 좌표 정보를 갖고 있고, 2) 자신이 원하는 정보만 지도에 얹고 싶은 경우에 아래와 같은 사진의 결과를 얻을 수 있습니다.
이번 글의 목적은 다음과 같습니다.
위 사진과 같은 지도이미지에 전세계에 위치한 발전소의 발전원별(색), 용량별(크기)로 분포도를 scatter plot
여기서 전세계 위치는 위도, 경도로 확인 가능하여야 함
우선 간단히 코딩을 하기 위해 필요한 환경을 공유합니다. 여기서 특징은 이 라이브러리가 더이상 pip를 지원하지 않고 업데이트를 하지 않아 conda 환경에서 설치하고 사용해야 한다는 점입니다. 그러다보니, 설치가 쉽지 않을 수 있는데 그냥 아래처럼 버전을 맞추고, 가상환경을 설정해 진행하는 것을 추천합니다.
위 코드를 실행하면 문제를 해결 가능하며, 문제가 있다면 아래 코드를 옮기기 전 이걸 먼저 실행해주세요.
이렇게 설치가 완료되면, 데이터셋을 적재하고 코드를 실행하겠습니다. 데이터셋은 World Resources Institute에 Global Power Plant Database 를 csv로 제공하고 있습니다. 확보한 데이터셋을 파이썬 환경에 올리고, 코드에 필요한 라이브러리들을 불러오겠습니다.
여기서 scickit learn의 label encoder를 활용했는데, 이는 발전원 데이터(텍스트)를 색으로 구분하고, 이를 위해 필요한 인코딩을 수행했습니다.
# 1. Load the data
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
from sklearn.preprocessing import LabelEncoder
global_power_df = pd.read_csv('global_power_plant_database.csv')
# Encooding with fuel types
le = LabelEncoder()
global_power_df['primary_fuel_encoded'] = le.fit_transform(global_power_df['primary_fuel'])
다음은 지도에 필요한 배경을 그리겠습니다. 아래 코드대로 작성하면, 처음에 봤던 세계전도같은 것이 global_m에 담기게 됩니다. 여기서 사이즈는 (160, 120)으로 설정하였는데, 적절한 크기로 설정하지 않으면 scatter plot의 버블이 지도에 비해 너무 커질 수도 있으니 주의하셔야 합니다..
파이썬이 제공하는 여러 편한 툴들을 활용해 시뮬레이션을 할 수 있을 때가 많다. 대표적으로 scipy를 활용해 linear optimization을 했었던 저번 포스트가 있었는데, 이번에는 그런 모듈을 활용하는 것은 아니고 간단한 사고실험 수준의 시뮬레이션을 해볼까 한다.
일반적으로, 투자를 할 때 자산군을 나눠 투자를 하는 것이 전체 포트폴리오 가치의 분산을 낮추고, 서로 다른 성격의 자산덕분에 여러 기회를 잃지 않는다는 점에서 권장된다. 여기서 시도해볼 것은 가격의 변동이 있는 주식과 현금을 나눠 투자하고, 가치가 변할 때마다 리밸런싱을 하는 상황을 가정한다.
순서는 다음과 같다.
1. 최초에 10,000만큼 자산을 보유하고 있고, 주식:현금 을 설정(여기서는 5:5)한다.
2. 주식 가격의 변동이 발생하고, 그 가격변동에 따른 자산의 변동이 발생한다.
3. 변동된 자산에 기존 설정한 비율에 맞춰 주식을 매도/매수하여 현금의 증감이 발생한다.
4. 시간 기준이 바뀌면, 2-3과정을 반복한다.
위 알고리즘에서 가장 핵심적인 2-3에 대해 살펴보자면, 다음과 같다.
먼저, 주식 가격의 변동부터 만들어 주자. 가격 변동 폭을 만들어주기 위해서 여러 방법에 대해 고민했는데, 지난 20년간 매일 수익의 변화가 정규분포에 가까운 모양임을 감안해 수익률의 변동을 np.random.normal을 이용해 만들어주기로 결정했다. (아마도 그이상의 시계열로 놓고 보더라도 비슷한 결과가 나오리라 생각된다.)
최초(0년차)부터 30년 후인 (30년차)까지 값을 조정할 수 있게끔 시계열(time_series)과 주식비중(stock_ratio), 그리고 최초 주식가격을 50으로 설정하는 것은 덤이다.
import numpy as np
import pandas as pd
stock_ratio = 0.5 # 주식비율 설정
time_series = 30 # 시계열 설정
# 수익률의 분포는 정규분포(normal distribution)로 가정
# %를 적용하기 편하게 -1~1 값으로 만들어 주기위해 10으로 나눔
random_return = np.random.normal(size=time_series+1)/10
price = [50] # 최초 주식가격 50으로 설정
for i in range(time_series):
new_price = price[i]*(1+random_return[i])
price.append(new_price)
price_dict = {'Price': price, '(n+1)Return(%)':random_return*100}
df= pd.DataFrame(data = price_dict)
df.head()
위 코드의 결과는 대략적으로 아래 사진처럼 나올 것이고, 실행때마다 전혀 다른 값이 나오는 점은 유의해야 한다.
초기 자산인주가의 변동이 발생하고 다음이 조금 어려울 수 있는데, 이는 하나씩 설명하기보다 유기적으로 연결되는 부분이라 한꺼번에 기술한다. 각 데이터들에 대한 설명부터 하자.
assets = [ 내 자산의 가치(asset) ] = 현금 + 주식계좌
stocks = [ 계좌에 있는 주식수 ]
stock_accnt = [ 내 주식 계좌의 가치 ] = stocks * price
cash = [ 현금 보유량 ] = asset - stocks * price
현재(당기) [i+1]
전기 [i]
각각 필요한 설명은 코드 밑에 주석을 첨부하였다. 그리고 결과는 아래와 같이 잘 나오고 있음을 확인할 수 있다. 다행스럽게도 전반적인 자산은 + 를 기록했음을 확인할 수 있다.
# 초기값 조건
initial_assets = 10000
initial_stocks = math.floor(initial_assets*stock_ratio/df['Price'].iloc[0])
initial_stock_accnt = initial_stocks * df['Price'].iloc[0]
initial_cash = initial_assets - initial_stock_accnt
# 각 관심있는 정보를 담을 Lists
assets = [initial_assets]
stocks = [initial_stocks]
stock_accnt = [initial_stock_accnt]
cash = [initial_cash]
for i in range(time_series):
delta_price = df['Price'].iloc[i+1]-df['Price'].iloc[i]
delta_asset = delta_price * stocks[i]
present_asset = assets[i] + delta_asset
assets.append(present_asset)
# 주식가격의 변동(delta_price)는 현재[i+1]와 전기[i]의 차이인데,
# 현재 내 자산의 가치는 전기에 가지고 있던 가격변동과 주식의 숫자를 곱한만큼(delta_asset) 변한다.
# 따라서, 당기 자산의 크기는 전기 자산의 크기와 자산의 변동만큼 더해주면 된다.
delta_cash = delta_asset * (1-stock_ratio)
delta_stocks = round(-delta_cash/df['Price'].iloc[i+1],0)
present_stocks = stocks[i] + delta_stocks
stocks.append(present_stocks)
# 위에서 결정된 delta_asset에 현금비중(1-주식비중)을 곱해 현금의 변화량을 측정
# 현금의 변화량에 현재 가격을 나눠 사야/팔아야하는 주식 수(주식의 증감)를 산정
# 주의해야할 것이 현금과 주식의 방향은 반대(즉, 현금이 빠져나간다는 것은 주식에 유입된다는 뜻)
# 이렇게 산정된 주식의 증감(delta_stocks)을 전기에 갖고 있던 주식수에 더해 현재 주식 수 계산
present_stock_accnt = present_stocks * df['Price'].iloc[i+1]
stock_accnt.append(present_stock_accnt)
present_cash = assets[i+1] - present_stock_accnt
cash.append(present_cash)
# 주식계좌가치(stock_accnt)는 현재 주식수와 현재 주식 가격의 곱
# 그리고 남는 현금은 현재 전체 자산에서 현재 주식계좌를 뺀 값
dictionary = {'Stocks': stocks,
'Stock Account' : stock_accnt,
'Cash' : cash,
'Assets': assets}
df_accnt = pd.DataFrame(data = dictionary)
summary = pd.concat([df, df_accnt], axis =1)
summary.tail()
그리고 이 결과를 matplotlib.pyplot으로 시각화하면 다음과 같다.
import matplotlib.pyplot as plt
plt.plot(summary.index, summary['Assets'])
plt.xlabel('Time')
plt.ylabel('Assets')
plt.show()
이를 여러번 시도할 수 있게끔, 전체 코드를 구성하였다. 여기에, 각 시도마다 나오는 계좌전체 수익률을 계산하기 위해 CAGR 계산하는 것만 더 추가했습니다.