따라하면서 쉽게 배우는 판다스

따라하면서 쉽게 배우는 판다스

2020, Feb 02    
intro_pandas

Intro to Pandas

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

판다스(Pandas)는 여러가지 유용한 데이터 자료구조를 제공하는 파이썬 라이브러리 입니다. 이번 콘텐츠에서는 판다스의 대표적 자료구조 형태인 SeriesDataFrame 구조를 학습하고 실제 금융 데이터 분석에 어떻게 적용되는지 알아보도록 하겠습니다. 주피터 노트북 환경에서 작성한 문서이기 때문에 주피터 노트북을 켜고 따라서 실습해 보시면 쉽게 이해하실 수 있습니다. 주피터 노트북의 기초적인 활용법과 NumPy 모듈의 기초적인 활용법, 판다스 Series 구조를 정리한 글은 아래 콘텐츠에 링크되어 있습니다. 참고하여 진행해 주시면 되겠습니다.

판다스(Pandas)를 사용하면 손쉬운 데이터 저장, 관리, 시각화를 할 수 있습니다. 몇 줄의 코드 작성을 통해 데이터를 직관적으로 다룰 수 있습니다. 저번 NumPy 콘텐츠에서 진행했던 포트폴리오 시뮬레이션을 다음과 같이 간단하게 수행할 수 있으며 시각화도 손쉽게 가능합니다.

In [8]:
returns = pd.DataFrame(np.random.normal(1.0, 0.03, (100, 10)))
prices = returns.cumprod()
prices.plot(figsize=(16,9))
plt.title('Randomly-generated Prices')
plt.xlabel('Time')
plt.ylabel('Price')
plt.legend();

판다스 자료구조

Series

판다스의 series 구조는 1차원 어레이 구조를 띄고 있으며 금융 시계열(time series)데이터를 다룰 때 매우 유용하게 사용됩니다. 시리즈는 다음과 같이 pandas.Series() 명령어를 통해 생성할수 있습니다.

In [9]:
s = pd.Series([1, 2, np.nan, 4, 5])
print(s)
0    1.0
1    2.0
2    NaN
3    4.0
4    5.0
dtype: float64

생성된 Series는 이름을 가지게 되며 따로 지정하지 않았다면 기본값인 None 값을 가지게 됩니다.

In [10]:
print(s.name)
None

다음과 같이 series.name에 접근하여 이름 지정이 가능합니다.

In [237]:
s.name = "Practice Series"
print(s.name)
Practice Series

위에서 s를 출력하였을때 우리가 지정한 값들이 0,1,2,3,4 라는 숫자에 매칭되어 있는것을 확인할수 있었습니다. 이렇게 데이터 값에 매칭되어 있는 값들을 시리즈의 인덱스(index)라고 합니다. 인덱스를 따로 지정하지 않았다면 기본값인 RangeIndex가 설정됩니다. RangeIndex는 0 부터 시작하는 정수열로 리스트의 순서와 같은 개념으로 이해하시면 됩니다.

In [13]:
print(s.index)
RangeIndex(start=0, stop=5, step=1)

날짜와 시간을 손쉽게 다룰 수 있도록 판다스는 date_range()와 같은 날짜 인덱싱 기능을 제공합니다. 아래와 같이 날짜 인덱스를 생성 가능합니다.

In [15]:
# len(s) : s의 길이
new_index = pd.date_range("2016-01-01", periods=len(s), freq="D")
print(new_index)
DatetimeIndex(['2016-01-01', '2016-01-02', '2016-01-03', '2016-01-04',
               '2016-01-05'],
              dtype='datetime64[ns]', freq='D')

인덱스의 길이는 해당 시리즈의 길이와 일치하여야 하기 때문에 len(s) 만큼의 기간(periods)를 할당하여 날짜 인덱스를 생성하였습니다. s의 인덱스를 새로 만든 new_index로 바꾸려면 다음과 같이 series.index에 접근하여 바로 변경이 가능합니다.

In [16]:
s.index = new_index
print(s.index)
DatetimeIndex(['2016-01-01', '2016-01-02', '2016-01-03', '2016-01-04',
               '2016-01-05'],
              dtype='datetime64[ns]', freq='D')
In [17]:
s
Out[17]:
2016-01-01    1.0
2016-01-02    2.0
2016-01-03    NaN
2016-01-04    4.0
2016-01-05    5.0
Freq: D, Name: Toy Series, dtype: float64

s의 인덱스가 날짜 형태로 바뀐것을 확인할 수 있습니다.

Series 값들에 접근하기

시리즈가 가지고 있는 데이터 값들에 접근하기 위해서는 iloc[]loc[] 명령어에 대한 이해가 필요합니다. iloc[]은 integer location의 약자로 "몇 번째에 있는 데이터에 접근하고 싶다!" 할때 사용합니다. loc[]은 시리즈의 인덱스를 통해 데이터에 접근하고자 할 때 사용됩니다.

In [18]:
print(s.iloc[0]) # s의 0번째 값을 출력합니다.
print(s.iloc[-1]) # s의 마지막 값을 출력합니다.
1.0
5.0

iloc[]을 사용하셨다면 이전에 배웠던 리스트 슬라이싱 기법을 적용하여 여러가지 방식으로 데이터에 접근이 가능합니다. 다음과 같이 시리즈를 손쉽게 역순으로 바꿀 수 있습니다.

In [21]:
# inverse the series s
s.iloc[::-1]
Out[21]:
2016-01-05    5.0
2016-01-04    4.0
2016-01-03    NaN
2016-01-02    2.0
2016-01-01    1.0
Freq: -1D, Name: Toy Series, dtype: float64

loc[]을 사용한다면 인덱스의 값을 통해 해당하는 곳의 데이터에 접근이 가능합니다. 다음과 같이 2016년 1월 1일에 해당하는 값에 접근이 가능합니다.

In [22]:
s.loc['2016-01-01']
Out[22]:
1.0

또는 날짜의 범위를 지정하여 해당하는 구간의 데이터에도 접근이 가능합니다.

In [23]:
s.loc['2016-01-02':'2016-01-04']
Out[23]:
2016-01-02    2.0
2016-01-03    NaN
2016-01-04    4.0
Freq: D, Name: Toy Series, dtype: float64

데이터 필터링

이번에는 데이터값을 필터링하여 특정한 부분에만 접근하고자 할 때 사용할 수 있는 방법들에 대해 알아보도록 하겠습니다. 위에서 생성한 s 라는 시리즈에서 데이터 값이 3보다 작은곳을 확인하고자 할때, 다음과 같이 확인이 가능합니다.

In [24]:
print(s<3)
2016-01-01     True
2016-01-02     True
2016-01-03    False
2016-01-04    False
2016-01-05    False
Freq: D, Name: Toy Series, dtype: bool

인덱스와 함께 해당 조건의 진위값(True or False)가 출력됩니다. 진위값이 참(True)인 곳만 확인하고자 한다면 아래와 같이 원래 시리즈인 s에 이를 넣어주면 됩니다.

In [25]:
print(s.loc[s<3])
2016-01-01    1.0
2016-01-02    2.0
Freq: D, Name: Toy Series, dtype: float64

출력된 결과를 살펴보면 s<3 에서 인덱스 값이 True 인 곳만 출력된 것을 확인 가능합니다. 논리 연산자를 활용하여 복잡한 조건에 대한 필터링도 가능합니다. 아래는 1보다 크고 3보다 작은 곳을 필터링 하는 코드 입니다.

In [26]:
print(s.loc[(s<3) & (s>1)])
2016-01-02    2.0
Freq: D, Name: Toy Series, dtype: float64

시계열 데이터 인덱싱

실제 주식 데이터를 읽어 시계열 데이터를 다루는 법들에 대해 정리해 봅시다. 판다스 데이터리더를 활용하여 애플 주식의 데이터를 읽어옵니다. 설치 방법과 자세한 내용은 (콘텐츠 링크 달기) 를 참고해 주세요.

In [238]:
import pandas_datareader as web
symbol = 'AAPL'
start = '2016-01-01'
end = '2019-03-04'
prices = web.DataReader(symbol,'iex', start, end)
prices.head()
Out[238]:
open high low close volume
date
2016-01-04 96.9111 99.5159 96.3350 99.4989 67649387
2016-01-05 99.8767 99.9711 96.7222 97.0055 55790992
2016-01-06 94.9749 96.6844 94.3233 95.1072 68457388
2016-01-07 93.1993 94.5688 91.0743 91.0932 81094428
2016-01-08 93.0766 93.6055 91.3860 91.5749 70798016
In [240]:
print(type(prices))
<class 'pandas.core.frame.DataFrame'>

위의 prices 는 판다스 DataFrame 타입을 가지고 있습니다. 이는 아래에서 정리하도록 하고 일단 애플의 종가데이터만 가져오도록 하겠습니다. 아래와 같이 종가(close) 데이터에 접근이 가능합니다.

In [241]:
close = prices['close']
print(type(close))
<class 'pandas.core.series.Series'>

close의 타입이 pandas.Series 임을 확인했습니다. 아래와 같이 .head() 명령어를 사용하여 제일 위의 데이터 5개를 볼 수 있습니다.

In [242]:
close.head()
Out[242]:
date
2016-01-04    99.4989
2016-01-05    97.0055
2016-01-06    95.1072
2016-01-07    91.0932
2016-01-08    91.5749
Name: close, dtype: float64

close 라는 시리즈의 이름을 'AAPL_close'로 설정해 줍니다.

In [243]:
close.name = 'AAPL_close'

close의 인덱스를 확인해 봅시다.

In [38]:
close.index
Out[38]:
Index(['2016-01-04', '2016-01-05', '2016-01-06', '2016-01-07', '2016-01-08',
       '2016-01-11', '2016-01-12', '2016-01-13', '2016-01-14', '2016-01-15',
       ...
       '2019-02-19', '2019-02-20', '2019-02-21', '2019-02-22', '2019-02-25',
       '2019-02-26', '2019-02-27', '2019-02-28', '2019-03-01', '2019-03-04'],
      dtype='object', name='date', length=796)

위의 출력 결과를 보면 dtype(데이터 타입)이 'object'라고 되어있습니다. 해당 인덱스의 첫번째 값과 그 타입을 아래와 같이 출력해 보도록 하겠습니다.

In [245]:
print(close.index[0])
print(type(close.index[0]))
2016-01-04
<class 'str'>

인덱스가 문자열(str) 타입을 가지고 있습니다. 이렇게 되면 close라는 시리즈를 활용하여 시계열 데이터를 다루는데 어려움이 생기게 됩니다. 시계열 데이터를 손쉽게 다루려면 컴퓨터가 '2016-01-04' 라는 값을 2016년 1월 4일 이라고 인식해야 합니다. 하지만 위의 경우 문자 그대로 '2016-01-04' 라는 문자열 값으로 인식하게 됩니다. 따라서 아래와 같이 pd.to_datetime() 함수를 활용하여 인덱스를 날짜 형태로 바꾸어 주는 작업이 필요합니다.

In [41]:
pd.to_datetime(close.index)
Out[41]:
DatetimeIndex(['2016-01-04', '2016-01-05', '2016-01-06', '2016-01-07',
               '2016-01-08', '2016-01-11', '2016-01-12', '2016-01-13',
               '2016-01-14', '2016-01-15',
               ...
               '2019-02-19', '2019-02-20', '2019-02-21', '2019-02-22',
               '2019-02-25', '2019-02-26', '2019-02-27', '2019-02-28',
               '2019-03-01', '2019-03-04'],
              dtype='datetime64[ns]', name='date', length=796, freq=None)
In [42]:
close.index = pd.to_datetime(close.index)
type(close.index[0])
Out[42]:
pandas._libs.tslibs.timestamps.Timestamp

close.index의 데이터 타입이 datetime64 로 바뀐것을 확인하였습니다. 이제 파이썬은 인덱스를 날짜로 인식하게 된것입니다.

판다스를 활용한 시계열 분석

위의 close 시리즈를 활용하여 간단한 분석을 진행해 보도록 하겠습니다. 먼저 그래프를 그려 간단하게 시각화가 가능합니다.

In [88]:
close.plot(figsize=(16,9)); # 그래프의 사이즈를 정해 줍니다.
plt.title(symbol + 'Close Price') # 제목을 설정합니다.
plt.ylabel('Close') # y축의 라벨을 설정합니다.
plt.xlabel('Date'); # x축의 라벨을 설정합니다.

pandas.Series.describe() 메서드를 활용해 간단한 통계적 정보를 확인 가능합니다.

In [89]:
print('Summary Statistics')
print(close.describe())
Summary Statistics
count    796.000000
mean     145.470924
std       38.014947
min       86.307900
25%      107.691450
50%      149.914900
75%      171.574550
max      230.275400
Name: close, dtype: float64

시리즈에 숫자(스칼라)를 곱하거나(나누거나) 더해서(빼서) 다음과 같이 쉽게 변형이 가능합니다.

In [91]:
edited_close = close * 10 - 10
edited_close.head()
Out[91]:
date
2016-01-04    984.989
2016-01-05    960.055
2016-01-06    941.072
2016-01-07    900.932
2016-01-08    905.749
Name: close, dtype: float64

시리즈 끼리의 기본적인 연산도 가능합니다. 시리즈 끼리 연산을 하면 같은 인덱스에 해당 하는 값들 끼리 연산을 한 뒤 결과를 출력합니다. 다음과 같이 시리즈를 더해보도록 하겠습니다.

In [253]:
a = pd.Series([1,2,3])
b = pd.Series([1,2])
a + b
Out[253]:
0    2.0
1    4.0
2    NaN
dtype: float64

같은 인덱스에 해당하는 값끼리 더하기 때문에 인덱스 0과 1번에 해당하는 값들은 더해져서 2와 4가 출력되었고 2번 인덱스의 경우 b라는 시리즈에 해당하는 인덱스의 값이 없기 때문에 더하기 연산을 수행할 수 없게되고 NaN(Not a Number) 값이 출력됩니다. 이렇게 NaN값이 출력되는 것을 방지하기 위해 아래와 같이 add() 연산과 fill_value를 0으로 설정하면 비어있는 값에 대해서는 0이 더해져 아래와 같이 출력됩니다.

In [260]:
a.add(b, fill_value=0)
Out[260]:
0    2.0
1    4.0
2    3.0
dtype: float64

곱하기의 경우도 같은 방식으로 계산이 가능합니다. 덧셈과 마찬가지로 특정 인덱스에 해당하는 값이 없어서 연산이 불가능하면 NaN이 출력됩니다.

In [259]:
a * b
Out[259]:
0    1.0
1    4.0
2    NaN
dtype: float64

곱하기 기호대신 multiply() 연산과 fill_value 값을 1로 설정하면 다음과 같은 결과를 얻을 수 있습니다.

In [258]:
a.multiply(b, fill_value=1)
Out[258]:
0    1.0
1    4.0
2    3.0
dtype: float64

아래와 같이 noise 텀을 만들어서 노이즈가 낀 가격 데이터를 만들 수 있습니다.

In [93]:
noise = pd.Series(np.random.normal(0,5,len(close)), index=close.index) + 20
noise.head()
Out[93]:
date
2016-01-04    18.023656
2016-01-05    23.609052
2016-01-06    19.278072
2016-01-07    18.127893
2016-01-08    16.816788
dtype: float64
In [96]:
noisy_price = close + noise
noisy_price.head()
Out[96]:
date
2016-01-04    117.522556
2016-01-05    120.614552
2016-01-06    114.385272
2016-01-07    109.221093
2016-01-08    108.391688
dtype: float64

시계열 데이터 분석에서 많이 사용되는 차분(difference)과 퍼센트 수익률(percent return)을 내장된 함수를 통해 손쉽게 계산이 가능합니다.

$$ Difference_t = Price_t - Price_{t-1} $$

diff() 메서드를 사용해 차분(difference)을 쉽게 계산 가능합니다.

$$ \text{Percent Change}_t = \frac{Price_t - Price_{t-1}}{Price_{t-1}} $$

pct_change() 메서드를 사용해 퍼센트 수익률(percent return)을 쉽계 계산 가능합니다.

In [262]:
add_returns = close.diff()[1:]
percent_returns = close.pct_change()[1:]
In [264]:
plt.title("Percent returns of " + symbol)
plt.xlabel("Date")
plt.ylabel("Percent Returns")
percent_returns.plot(figsize=(16,9));

시계열 데이터를 분석할 때 특정기간(window)를 rolling 하면서 값들을 계산해야 할 상황이 많이 있습니다. 대표적으로 주식 차트 분석에서 널리 사용되는 이동평균선이 있습니다. 시리즈에 내장된 rolling 기능을 활용해 이런 값들을 손쉽게 계산할 수 있습니다.

In [265]:
# 20일간의 기간(window)를 설정하여 rolling mean을 계산합니다.
moving_average = close.rolling(window=20).mean()
moving_average.name = '20-day moving average'
In [108]:
close.plot(figsize=(16,9))
moving_average.plot();
plt.title(symbol + "Price")
plt.xlabel("Date")
plt.ylabel("Price")
plt.legend();

이동평균 뿐만 아니라 표준편차도 같은 방식으로 rolling 하면서 계산이 가능합니다.

In [109]:
moving_std = close.rolling(window=20).std()
moving_std.name = '20-day moving volatillity'
In [114]:
moving_std.plot(figsize=(16,9));
plt.title(moving_std.name);
plt.xlabel('Date')
plt.ylabel('Standard Deviation');

넘파이의 많은 계산 함수들이 시리즈에 내장되어 있어 손쉽게 사용가능합니다. 중간값인 median을 다음과 같이 2가지 방법으로 계산할 수 있습니다.

In [115]:
print(np.median(mult_returns))
0.0007651405963791102
In [116]:
print(mult_returns.median())
0.0007651405963791102

Intro to Pandas DataFrame

In [2]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from jupyterthemes import jtplot
jtplot.style('grade3')

pandas.DataFrame

저번 콘텐츠에서 배운 pandas.Series의 개념들은 판다스의 또다른 대표적 자료구조인 pandas.DataFrame에서도 그대로 적용이 가능합니다. 데이터 프레임은 특정 인덱스를 갖는 2차원 테이블 이라고 생각하시면 쉽게 이해할 수 있습니다. 인덱스인 날짜와 해당 날짜의 시가(open), 고가(high), 저가(low), 종가(close), 거래량(volume)의 정보를 가지고 있는 일반적인 주가데이터나 엑셀의 스프레드시트를 떠올리시면 됩니다. 저번 콘텐츠에서 아래와 같이 pandas_datareader모듈을 사용하여 가격정보를 불러왔었습니다. prices라는 변수에 저장된 가격정보는 데이터프레임 타입을 가지고 있습니다.

In [3]:
import pandas_datareader as web
symbol = 'AAPL'
start = '2016-01-01'
end = '2019-03-04'
prices = web.DataReader(symbol,'iex', start, end)
prices.head()
Out[3]:
open high low close volume
date
2016-01-04 96.9111 99.5159 96.3350 99.4989 67649387
2016-01-05 99.8767 99.9711 96.7222 97.0055 55790992
2016-01-06 94.9749 96.6844 94.3233 95.1072 68457388
2016-01-07 93.1993 94.5688 91.0743 91.0932 81094428
2016-01-08 93.0766 93.6055 91.3860 91.5749 70798016
In [4]:
print(type(prices))
<class 'pandas.core.frame.DataFrame'>

DataFrmae은 pandas.DataFrame() 함수를 사용해 생성이 가능하며 함수안에 dictionary 또는 넘파이 ndarray를 입력하면 해당 객체가 dataFrame으로 변경됩니다. 또한 여러개의 Series를 pandas.concat() 메서드를 활용하여 dataFrame으로 합성이 가능합니다. 이 외에 다른 기능들은 판다스 데이터프레임 공식문서를 참고하시면 됩니다. 여기서는 dictionary를 데이터 프레임으로 변환해 보겠습니다.

In [5]:
dict_data = {
    'a' : [1, 2, 3, 4, 5],
    'b' : ['L', 'K', 'J', 'M', 'Z'],
    'c' : np.random.normal(0, 1, 5)
}
print(dict_data)
{'a': [1, 2, 3, 4, 5], 'b': ['L', 'K', 'J', 'M', 'Z'], 'c': array([-0.05548304, -0.87560184,  0.25078361,  0.42431009,  0.6577422 ])}

Series를 생성할 때와 같이 손쉽게 날짜 인덱싱이 가능합니다.

In [6]:
frame_data = pd.DataFrame(dict_data, index=pd.date_range('2016-01-01', periods=5))
frame_data
Out[6]:
a b c
2016-01-01 1 L -0.055483
2016-01-02 2 K -0.875602
2016-01-03 3 J 0.250784
2016-01-04 4 M 0.424310
2016-01-05 5 Z 0.657742

위에서 언급했던 여러개의 Series 구조를 하나의 DataFrame으로 합칠수 있습니다. pandas.concat() 메서드를 활용할 경우 axis라는 키워드를 통해 합칠 방향(0:세로방향, 1:가로방향)을 정할 수 있으며, 자동으로 같은 인덱스끼리 데이터가 정렬되어 하나의 데이터프레임으로 합쳐지게 됩니다. 아래 예제의 경우 두 시리즈 모두 같은 정수형 인덱스를 가지기 때문에 다음과 같이 합쳐지게 됩니다.

In [11]:
s_1 = pd.Series([2, 4, 6, 8, 10], name='Evens')
s_2 = pd.Series([1, 3, 5, 7, 9], name="Odds")
numbers = pd.concat([s_1, s_2], axis=1)
numbers
Out[11]:
Evens Odds
0 2 1
1 4 3
2 6 5
3 8 7
4 10 9

위에서 불러온 prices 데이터 프레임을 바탕으로 몇가지 명령어들을 살펴보도록 하겠습니다. DataFrame.head() 명령어를 통해 상위 5개의 데이터를 살펴볼 수 있습니다. 괄호안에 숫자를 입력하면 해당 개수만큼 데이터를 가져옵니다.

In [7]:
prices.head()
Out[7]:
open high low close volume
date
2016-01-04 96.9111 99.5159 96.3350 99.4989 67649387
2016-01-05 99.8767 99.9711 96.7222 97.0055 55790992
2016-01-06 94.9749 96.6844 94.3233 95.1072 68457388
2016-01-07 93.1993 94.5688 91.0743 91.0932 81094428
2016-01-08 93.0766 93.6055 91.3860 91.5749 70798016

DataFrame.columns 명령어를 사용해 해당 데이터프레임의 열(column) 목록을 확인 가능하며 다른 리스트 값을 집어넣어 변경또한 가능합니다.

In [127]:
prices.columns
Out[127]:
Index(['open', 'high', 'low', 'close', 'volume'], dtype='object')

시리즈와 같이 인덱스(index)에 접근이 가능하며 저번 콘텐츠에서 처럼 날짜형식으로 인덱스를 바꾸어 주도록 하겠습니다.

In [129]:
prices.index = pd.to_datetime(prices.index)
prices.index
Out[129]:
DatetimeIndex(['2016-01-04', '2016-01-05', '2016-01-06', '2016-01-07',
               '2016-01-08', '2016-01-11', '2016-01-12', '2016-01-13',
               '2016-01-14', '2016-01-15',
               ...
               '2019-02-19', '2019-02-20', '2019-02-21', '2019-02-22',
               '2019-02-25', '2019-02-26', '2019-02-27', '2019-02-28',
               '2019-03-01', '2019-03-04'],
              dtype='datetime64[ns]', name='date', length=796, freq=None)

DataFrame.values 명령어를 통해 인덱스와 columns를 제외한 내용물값 데이터만을 받아올 수 있습니다.

In [130]:
prices.values
Out[130]:
array([[9.6911100e+01, 9.9515900e+01, 9.6335000e+01, 9.9498900e+01,
        6.7649387e+07],
       [9.9876700e+01, 9.9971100e+01, 9.6722200e+01, 9.7005500e+01,
        5.5790992e+07],
       [9.4974900e+01, 9.6684400e+01, 9.4323300e+01, 9.5107200e+01,
        6.8457388e+07],
       ...,
       [1.7432000e+02, 1.7491000e+02, 1.7292000e+02, 1.7315000e+02,
        2.8215416e+07],
       [1.7428000e+02, 1.7515000e+02, 1.7289000e+02, 1.7497000e+02,
        2.5886167e+07],
       [1.7569000e+02, 1.7775000e+02, 1.7397000e+02, 1.7585000e+02,
        2.7436203e+07]])

DataFrame.values 를 통해 불러온 값은 넘파이 어레이(ndarray) 타입을 가집니다.

In [131]:
type(prices.values)
Out[131]:
numpy.ndarray

DataFrame 값들에 접근하기

DataFrame은 2차원 테이블 형태입니다. DataFrame의 특정 열이나 행에 접근할 수 있는 방법들에 대해 알아보도록 하겠습니다. 먼저 특정 열(column)에 접근하고자 할 경우 다음과 같이 DataFrame.열이름 과 같이 해당 열에 접근이 가능합니다. 위에서 불러온 테이블에서 종가(close)가 담겨있는 열에 접근해 보도록 하겠습니다.

In [133]:
prices.close.head()
Out[133]:
date
2016-01-04    99.4989
2016-01-05    97.0055
2016-01-06    95.1072
2016-01-07    91.0932
2016-01-08    91.5749
Name: close, dtype: float64

또다른 방법으로는 DataFrame.[열이름] 과 같은 방식으로도 접근이 가능합니다.

In [134]:
prices['close'].head()
Out[134]:
date
2016-01-04    99.4989
2016-01-05    97.0055
2016-01-06    95.1072
2016-01-07    91.0932
2016-01-08    91.5749
Name: close, dtype: float64

마지막 방법으로 아래와 같이 loc[] 명령어를 사용하여 접근이 가능합니다. 이 방법이 판다스에서 가장 권장하는 방법입니다.

In [136]:
prices.loc[:,'close'].head()
Out[136]:
date
2016-01-04    99.4989
2016-01-05    97.0055
2016-01-06    95.1072
2016-01-07    91.0932
2016-01-08    91.5749
Name: close, dtype: float64

DataFrame에서 특정 열에 접근했다면 그 결과는 무조건 Series 타입을 가지게 됩니다.

In [137]:
print(type(prices.close))
print(type(prices['close']))
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>

Series에서와 달리 DataFrame은 2차원 구조를 가지기 때문에 loc[] 메서드에 들어가는 입력값이 2개 입니다. 아래와 같이 열이름으로 구성된 리스트를 입력하여 여러 열을 한번에 가져올 수 있습니다.

In [139]:
prices.loc[:,['close','volume']].head()
Out[139]:
close volume
date
2016-01-04 99.4989 67649387
2016-01-05 97.0055 55790992
2016-01-06 95.1072 68457388
2016-01-07 91.0932 81094428
2016-01-08 91.5749 70798016

Series에서 처럼 loc[] 명령어에 인덱스 범위를 입력하여 해당되는 값들에 접근이 가능합니다.

In [142]:
prices.loc['2016-01-01':'2016-01-10']
Out[142]:
open high low close volume
date
2016-01-04 96.9111 99.5159 96.3350 99.4989 67649387
2016-01-05 99.8767 99.9711 96.7222 97.0055 55790992
2016-01-06 94.9749 96.6844 94.3233 95.1072 68457388
2016-01-07 93.1993 94.5688 91.0743 91.0932 81094428
2016-01-08 93.0766 93.6055 91.3860 91.5749 70798016

위의 두 예제를 조합하면 다음과같이 원하는 인덱스와 원하는 열의 데이터에 접근이 가능합니다.

In [148]:
prices.loc['2016-01-01':'2016-01-10', ['close', 'volume']]
Out[148]:
close volume
date
2016-01-04 99.4989 67649387
2016-01-05 97.0055 55790992
2016-01-06 95.1072 68457388
2016-01-07 91.0932 81094428
2016-01-08 91.5749 70798016

다음과 같이 범위를 지정하면 2016년 1월 한달의 애플 주가데이터에 접근이 가능합니다.

In [143]:
prices.loc['2016-01':'2016-01']
Out[143]:
open high low close volume
date
2016-01-04 96.9111 99.5159 96.3350 99.4989 67649387
2016-01-05 99.8767 99.9711 96.7222 97.0055 55790992
2016-01-06 94.9749 96.6844 94.3233 95.1072 68457388
2016-01-07 93.1993 94.5688 91.0743 91.0932 81094428
2016-01-08 93.0766 93.6055 91.3860 91.5749 70798016
2016-01-11 93.4732 93.5582 91.9338 93.0577 49739377
2016-01-12 94.9655 95.0977 93.3504 94.4083 49154227
2016-01-13 94.7483 95.5699 91.8960 91.9810 62439631
2016-01-14 92.5193 94.8994 90.4226 93.9927 63170127
2016-01-15 90.8571 92.2832 90.0637 91.7354 79833891
2016-01-19 92.9443 93.1710 90.1960 91.2915 53087747
2016-01-20 89.8182 92.7363 88.2315 91.4143 72334416
2016-01-21 91.6693 92.4438 89.6671 90.9515 52161463
2016-01-22 93.1521 95.8249 92.9066 95.7872 65800467
2016-01-25 95.8816 95.8911 93.6999 93.9171 51794525
2016-01-26 94.3799 95.2772 92.6232 94.4366 75077002
2016-01-27 90.7060 91.2622 88.1559 88.2315 133369674
2016-01-28 88.5809 89.2704 87.2587 88.8643 55678825
2016-01-29 89.5254 91.9338 89.1098 91.9338 64416504

iloc[] 또한 Series에서처럼 동일하게 사용이 가능합니다.

In [151]:
prices.iloc[0:10,1]
Out[151]:
date
2016-01-04    99.5159
2016-01-05    99.9711
2016-01-06    96.6844
2016-01-07    94.5688
2016-01-08    93.6055
2016-01-11    93.5582
2016-01-12    95.0977
2016-01-13    95.5699
2016-01-14    94.8994
2016-01-15    92.2832
Name: high, dtype: float64

다음과 같이 리스트 슬라이싱 기법을 활용하여 홀수번 행(row)에 접근이 가능합니다. iloc[]은 0번 부터 시작하기 때문에 아래와 같이 접근할 경우 홀수번째 행에 접근함을 의미합니다.

In [17]:
prices.iloc[::2].head()
Out[17]:
open high low close volume
date
2016-01-04 96.9111 99.5159 96.3350 99.4989 67649387
2016-01-06 94.9749 96.6844 94.3233 95.1072 68457388
2016-01-08 93.0766 93.6055 91.3860 91.5749 70798016
2016-01-12 94.9655 95.0977 93.3504 94.4083 49154227
2016-01-14 92.5193 94.8994 90.4226 93.9927 63170127

데이터 필터링

Series에서 처럼 DataFrame도 데이터를 필터링 할 수 있는 기능을 제공합니다. prices 데이터로 캔들차트를 그렸을 때 양봉이 되는 날을 필터링 해보도록 하겠습니다. 캔들차트에 대한 설명은 주식 왕초보자들을 위한 용어 설명 2 - 시고저종, 거래량, 그리고 거래대금 콘텐츠를 참고해 주세요!

파이썬으로 캔들차트 그리는 방법은 [누구나 따라 하는 금융데이터 분석] - 파이썬에서 캔들차트 그리기 게시물을 참고해 주세요!

특정한 날의 캔들차트가 양봉이라는 것은 해당일의 종가(close)가 시가(open)보다 높게 장이 마감했다는것 입니다. 이를 아래와 같이 직관적인 조건식으로 필터링이 가능합니다.

In [157]:
prices.loc[prices.close > prices.open].head()
Out[157]:
open high low close volume
date
2016-01-04 96.9111 99.5159 96.3350 99.4989 67649387
2016-01-06 94.9749 96.6844 94.3233 95.1072 68457388
2016-01-14 92.5193 94.8994 90.4226 93.9927 63170127
2016-01-15 90.8571 92.2832 90.0637 91.7354 79833891
2016-01-20 89.8182 92.7363 88.2315 91.4143 72334416

조건식에 논리연산자를 추가하여 더욱 복잡한 필터링이 가능합니다. 종가가 시가대비 3% 이상 상승한 양봉을 찾아보도록 하겠습니다. 위의 양봉 조건에 시가대비 종가의 상승률이 0.03 보다 크다는 조건을 추가하여 필터링이 가능합니다.

In [160]:
prices.loc[(prices.close > prices.open) &
          ((prices.close - prices.open)/prices.open > 0.03)].head()
Out[160]:
open high low close volume
date
2016-11-16 103.0161 106.4242 102.9195 106.1925 58840522
2018-02-06 151.8884 160.6095 151.0741 159.9326 68243838
2018-04-04 162.4069 169.4300 162.2986 169.0360 34605489
2018-05-04 175.5764 181.4864 175.4976 181.0727 56201317
2018-08-02 198.3333 206.0459 198.1059 205.0670 62404012

데이터 프레임 조작하기

기존의 데이터 프레임에 다른 데이터를 추가하거나 특정한 곳을 제거하려면 어떻게 해야 할까요? 그 방법들을 알아보도록 하겠습니다. 먼저 prices DataFrame에서 거래량 정보를 volume이라는 변수에 따로 저장해 둡니다.

In [18]:
volume = prices.loc[:,'volume']
type(volume)
Out[18]:
pandas.core.series.Series

특정 열(column)을 제거하려면 DataFrame.drop('열 이름', axis=1)이라고 입력한 뒤 실행하시면 됩니다. 아래와 같이 prices 에서 거래량 정보를 제거하겠습니다. 출력결과 거래량 정보가 삭제되었음을 확인가능합니다.

In [164]:
prices = prices.drop('volume', axis=1)
prices.head()
Out[164]:
open high low close
date
2016-01-04 96.9111 99.5159 96.3350 99.4989
2016-01-05 99.8767 99.9711 96.7222 97.0055
2016-01-06 94.9749 96.6844 94.3233 95.1072
2016-01-07 93.1993 94.5688 91.0743 91.0932
2016-01-08 93.0766 93.6055 91.3860 91.5749

DataFrame에 새로운 열을 추가하고자 할 때에는 단순히 DataFrame.loc[:, '추가할 열 이름']에 추가할 데이터를 할당해 주면 됩니다. 제거했던 거래량 정보(volume : Series)를 다시 prices DataFrame에 추가해 보도록 하겠습니다. 출력결과 거래량(volume) 열이 추가된것을 확인가능합니다.

In [166]:
prices.loc[:,'volume'] = volume
prices.head()
Out[166]:
open high low close volume
date
2016-01-04 96.9111 99.5159 96.3350 99.4989 67649387
2016-01-05 99.8767 99.9711 96.7222 97.0055 55790992
2016-01-06 94.9749 96.6844 94.3233 95.1072 68457388
2016-01-07 93.1993 94.5688 91.0743 91.0932 81094428
2016-01-08 93.0766 93.6055 91.3860 91.5749 70798016

여러 DataFrame을 합치고자 할 경우 pandas.concat() 메서드를 활용하여 병합이 가능합니다. 아래와 같이 prices 데이터에서 두 개의 DataFrame을 분리해 봅시다.

In [19]:
open_and_high = prices.loc[:, ['open', 'high']] # 시가와 고가를 담은 데이터프레임
low_and_close = prices.loc[:, ['low', 'close']] # 저가와 종가를 담은 데이터프레임

위의 두 DataFrame을 pd.concat() 메서드를 활용하여 아래와 같이 합칠수 있습니다.

In [20]:
ohlc = pd.concat([open_and_high, low_and_close], axis=1)
ohlc.head()
Out[20]:
open high low close
date
2016-01-04 96.9111 99.5159 96.3350 99.4989
2016-01-05 99.8767 99.9711 96.7222 97.0055
2016-01-06 94.9749 96.6844 94.3233 95.1072
2016-01-07 93.1993 94.5688 91.0743 91.0932
2016-01-08 93.0766 93.6055 91.3860 91.5749

데이터프레임을 활용한 간단한 시계열 분석

Series와 같이 DataFrame에도 시계열 데이터 분석을 편리하게 해줄 내장함수들이 많이 있습니다. 이를 활용하면 데이터프레임에 들어있는 여러 시계열 데이터(time series data)에 동시에 연산들을 적용할 수 있습니다. 위의 판다스 시리즈 분석에 쓰인 방법들과 거의 비슷하니 천천히 따라오시면 이해하시기 쉬울것 입니다.

DataFrame.plot() 메서드를 활용하여 여러가지 데이터를 한번에 차팅이 가능합니다. 열(column)이름이 자동으로 범례(legend)로 지정되어 있음을 확인가능합니다.

In [21]:
# 캔들차트 그리기는 링크 걸기
ohlc.plot(figsize=(16,9))
plt.title(symbol + ' OHLC')
plt.ylabel('Price')
plt.xlabel('Date');

Series에서 사용하였던 기본적인 통계량 계산 함수들도 그래도 사용가능합니다. 여기서 Series와 다른점은 괄호안에 axis라는 파라미터가 들어간다는것 입니다. 이는 해당 함수를 계산하는 방향을 나타내며 행(row)을 따라가며 계산을 할 경우 axis=0, 열(column)을 따라가며 계산할 경우 axis=1을 입력해 주면 됩니다. 아래 예제를 보면 쉽게 이해가 가능합니다.

In [22]:
prices.mean(axis=0)
Out[22]:
open      1.454032e+02
high      1.466543e+02
low       1.441922e+02
close     1.454709e+02
volume    3.313652e+07
dtype: float64
In [23]:
prices.std(axis=0)
Out[23]:
open      3.805207e+01
high      3.843831e+01
low       3.761731e+01
close     3.801495e+01
volume    1.518350e+07
dtype: float64

Series와 마찬가지로 describe() 메서드를 활용하여 요약된 통계량 정보를 한눈에 확인 가능합니다.

In [24]:
prices.describe()
Out[24]:
open high low close volume
count 796.000000 796.000000 796.000000 796.000000 7.960000e+02
mean 145.403219 146.654327 144.192244 145.470924 3.313652e+07
std 38.052073 38.438314 37.617308 38.014947 1.518350e+07
min 85.983000 87.578500 85.476700 86.307900 1.147592e+07
25% 107.283450 108.485500 106.716250 107.691450 2.331280e+07
50% 149.929500 150.813550 148.990900 149.914900 2.879982e+07
75% 171.368400 172.467850 170.107975 171.574550 3.836883e+07
max 228.995300 231.664500 228.003100 230.275400 1.333697e+08

스칼라 계산도 Series와 마찬가지로 각각 데이터에 적용됩니다.

In [188]:
(100 * prices - 100).head()
Out[188]:
open high low close volume
date
2016-01-04 9591.11 9851.59 9533.50 9849.89 6764938600
2016-01-05 9887.67 9897.11 9572.22 9600.55 5579099100
2016-01-06 9397.49 9568.44 9332.33 9410.72 6845738700
2016-01-07 9219.93 9356.88 9007.43 9009.32 8109442700
2016-01-08 9207.66 9260.55 9038.60 9057.49 7079801500

게시글의 첨부파일을 보시면 close_data.csv 라는 파일이 있습니다. 해당 엑셀파일은 코스피, 삼성전자 그리고 SK하이닉스의 2018년 동안의 수정종가를 담고있는 파일입니다.

pandas는 엑셀 csv 파일을 DataFrame으로 읽어올 수 있는 기능을 제공합니다. 첨부파일에 있는 csv파일을 현재 실행중인 주피터노트북이 저장된 폴더에 저장해 주시고 다음과 같이 pd.read_csv함수를 활용해 csv파일의 데이터를 읽어 옵니다.

In [26]:
# dt열을 인덱스 열로 지정하여 데이터를 읽어옵니다.
close_prices = pd.read_csv('close_data.csv', index_col='dt') 
close_prices.head()
Out[26]:
KOSPI_close SAMSUNG_close SK_Hynix_Close
dt
2018-01-02 2479.65 51020.0 76600.0
2018-01-03 2486.35 51620.0 77700.0
2018-01-04 2466.46 51080.0 77100.0
2018-01-05 2497.52 52120.0 79300.0
2018-01-08 2513.28 52020.0 78200.0
In [31]:
close_prices.tail() # 마지막 5개의 데이터를 확인합니다.
Out[31]:
KOSPI_close SAMSUNG_close SK_Hynix_Close
dt
2018-12-21 2061.49 38650.0 60000.0
2018-12-24 2055.01 38800.0 60400.0
2018-12-26 2028.01 38350.0 60100.0
2018-12-27 2028.44 38250.0 61600.0
2018-12-28 2041.04 38700.0 60500.0

csv 파일을 데이터가 정상적으로 읽어졌음을 확인하였습니다. 인덱스의 데이터 타입을 체크해 보도록 하겠습니다.

In [33]:
type(close_prices.index[0])
Out[33]:
str

저번처럼 문자열 타입이기 때문에 파이썬이 날짜로 인식하도록 아래와 같이 인덱스를 datatime형식으로 변경해 줍니다.

In [34]:
close_prices.index = pd.to_datetime(close_prices.index)

각 종목별 퍼센트 수익률을 시리즈에서 처럼 pct_change() 메서드를 활용해 손쉽게 계산 가능합니다.

In [36]:
mult_returns = close_prices.pct_change()[1:]
mult_returns.columns = ['KOSPI_return','SAMSUNG_return','SK_Hynix_return']
mult_returns.head()
Out[36]:
KOSPI_return SAMSUNG_return SK_Hynix_return
dt
2018-01-03 0.002702 0.011760 0.014360
2018-01-04 -0.008000 -0.010461 -0.007722
2018-01-05 0.012593 0.020360 0.028534
2018-01-08 0.006310 -0.001919 -0.013871
2018-01-09 -0.001214 -0.031142 -0.016624

종목간의 직관적 가격 움직임의 비교를 위해 같은 스케일에서 움직이도록 정규화를 진행합니다. norm_returns 라는 새로운 DataFrame에 종목별로 정규화 시킨 값들을 저장하여 그래프를 출력합니다.

In [42]:
norm_returns = (mult_returns - mult_returns.mean(axis=0))/mult_returns.std(axis=0)
norm_returns.plot(figsize=(16,9));

각 종목의 20일 이동평균선을 구해보도록 하겠습니다. 시리즈에서 처럼 rolling(window=20)과 같은 메서드를 활용하여 손쉽게 계산이 가능합니다.

In [40]:
moving_average = close_prices.rolling(window=20).mean()
moving_average.columns = ['KOSPI-MA20','SAMSUNG-MA20','SK_Hynix-MA20']
In [41]:
moving_average.plot(figsize=(16,9))
plt.title("20 Days Moving Average of Close Prices")
plt.xlabel("Date")
plt.ylabel("Price")
plt.legend();

</html>