DataFrame 시계열 자료 다루기

@winuss · October 31, 2020 · 7 min read

DatetimeIndex 인덱스

시계열 자료는 인덱스가 날짜 혹은 시간인 데이터를 말한다. Pandas에서 시계열 자료를 생성하려면 인덱스를 DatetimeIndex 자료형으로 만들어야 한다. DatetimeIndex는 특정한 순간에 기록된 타임스탬프(timestamp) 형식의 시계열 자료를 다루기 위한 인덱스이다. 타임스탬프 인덱스의 라벨값이 반드시 일정한 간격일 필요는 없다.

DatetimeIndex 인덱스는 다음과 같은 보조 함수를 사용하여 생성한다.

  • pd.to_datetime 함수
  • pd.date_range 함수

pd.to_datetime 함수를 쓰면 날짜/시간을 나타내는 문자열을 자동으로 datetime 자료형으로 바꾼 후 DatetimeIndex 자료형 인덱스를 생성한다.

import pandas as pd 
date_str = ["2018, 1, 1", "2018, 1, 4", "2018, 1, 5", "2018, 1, 6"]
idx = pd.to_datetime(date_str)
idx
DatetimeIndex(['2018-01-01', '2018-01-04', '2018-01-05', '2018-01-06'], dtype='datetime64[ns]', freq=None)

이렇게 만들어진 인덱스를 사용하여 시리즈나 데이터프레임을 생성하면 된다.

import numpy as np 
np.random.seed(0)
s = pd.Series(np.random.randn(4), index=idx)
s
2018-01-01    1.764052
2018-01-04    0.400157
2018-01-05    0.978738
2018-01-06    2.240893
dtype: float64

pd.to_datetime 함수를 쓰면 모든 날짜/시간을 일일히 입력할 필요없이 시작일과 종료일 또는 시작일과 기간을 입력하면 범위 내의 인덱스를 생성해 준다.

pd.date_range("2018-4-1", "2018-4-30")
DatetimeIndex(['2018-04-01', '2018-04-02', '2018-04-03', '2018-04-04',
               '2018-04-05', '2018-04-06', '2018-04-07', '2018-04-08',
               '2018-04-09', '2018-04-10', '2018-04-11', '2018-04-12',
               '2018-04-13', '2018-04-14', '2018-04-15', '2018-04-16',
               '2018-04-17', '2018-04-18', '2018-04-19', '2018-04-20',
               '2018-04-21', '2018-04-22', '2018-04-23', '2018-04-24',
               '2018-04-25', '2018-04-26', '2018-04-27', '2018-04-28',
               '2018-04-29', '2018-04-30'],
              dtype='datetime64[ns]', freq='D')
pd.date_range(start="2018-4-1", periods=30)
DatetimeIndex(['2018-04-01', '2018-04-02', '2018-04-03', '2018-04-04',
               '2018-04-05', '2018-04-06', '2018-04-07', '2018-04-08',
               '2018-04-09', '2018-04-10', '2018-04-11', '2018-04-12',
               '2018-04-13', '2018-04-14', '2018-04-15', '2018-04-16',
               '2018-04-17', '2018-04-18', '2018-04-19', '2018-04-20',
               '2018-04-21', '2018-04-22', '2018-04-23', '2018-04-24',
               '2018-04-25', '2018-04-26', '2018-04-27', '2018-04-28',
               '2018-04-29', '2018-04-30'],
              dtype='datetime64[ns]', freq='D')

freq 인수로 특정한 날짜만 생성되도록 할 수도 있다. 많이 사용되는 freq 인수값은 다음과 같다.

  • s: 초
  • T: 분
  • H: 시간
  • D: 일(day)
  • B: 주말이 아닌 평일
  • W: 주(일요일)
  • W-MON: 주(월요일)
  • M: 각 달(month)의 마지막 날
  • MS: 각 달의 첫날
  • BM: 주말이 아닌 평일 중에서 각 달의 마지막 날
  • BMS: 주말이 아닌 평일 중에서 각 달의 첫날
  • WOM-2THU: 각 달의 두번째 목요일
  • Q-JAN: 각 분기의 첫달의 마지막 날
  • Q-DEC: 각 분기의 마지막 달의 마지막 날

보다 자세한 내용은 다음 웹사이트를 참조하자.

pd.date_range("2018-4-1", "2018-4-30", freq="B") # 평일만..
DatetimeIndex(['2018-04-02', '2018-04-03', '2018-04-04', '2018-04-05',
               '2018-04-06', '2018-04-09', '2018-04-10', '2018-04-11',
               '2018-04-12', '2018-04-13', '2018-04-16', '2018-04-17',
               '2018-04-18', '2018-04-19', '2018-04-20', '2018-04-23',
               '2018-04-24', '2018-04-25', '2018-04-26', '2018-04-27',
               '2018-04-30'],
              dtype='datetime64[ns]', freq='B')
pd.date_range("2018-1-1", "2018-12-31", freq="W") # 일요일만
DatetimeIndex(['2018-01-07', '2018-01-14', '2018-01-21', '2018-01-28',
               '2018-02-04', '2018-02-11', '2018-02-18', '2018-02-25',
               '2018-03-04', '2018-03-11', '2018-03-18', '2018-03-25',
               '2018-04-01', '2018-04-08', '2018-04-15', '2018-04-22',
               '2018-04-29', '2018-05-06', '2018-05-13', '2018-05-20',
               '2018-05-27', '2018-06-03', '2018-06-10', '2018-06-17',
               '2018-06-24', '2018-07-01', '2018-07-08', '2018-07-15',
               '2018-07-22', '2018-07-29', '2018-08-05', '2018-08-12',
               '2018-08-19', '2018-08-26', '2018-09-02', '2018-09-09',
               '2018-09-16', '2018-09-23', '2018-09-30', '2018-10-07',
               '2018-10-14', '2018-10-21', '2018-10-28', '2018-11-04',
               '2018-11-11', '2018-11-18', '2018-11-25', '2018-12-02',
               '2018-12-09', '2018-12-16', '2018-12-23', '2018-12-30'],
              dtype='datetime64[ns]', freq='W-SUN')
pd.date_range("2018-1-1", "2018-12-31", freq="W-MON") # 월요일
DatetimeIndex(['2018-01-01', '2018-01-08', '2018-01-15', '2018-01-22',
               '2018-01-29', '2018-02-05', '2018-02-12', '2018-02-19',
               '2018-02-26', '2018-03-05', '2018-03-12', '2018-03-19',
               '2018-03-26', '2018-04-02', '2018-04-09', '2018-04-16',
               '2018-04-23', '2018-04-30', '2018-05-07', '2018-05-14',
               '2018-05-21', '2018-05-28', '2018-06-04', '2018-06-11',
               '2018-06-18', '2018-06-25', '2018-07-02', '2018-07-09',
               '2018-07-16', '2018-07-23', '2018-07-30', '2018-08-06',
               '2018-08-13', '2018-08-20', '2018-08-27', '2018-09-03',
               '2018-09-10', '2018-09-17', '2018-09-24', '2018-10-01',
               '2018-10-08', '2018-10-15', '2018-10-22', '2018-10-29',
               '2018-11-05', '2018-11-12', '2018-11-19', '2018-11-26',
               '2018-12-03', '2018-12-10', '2018-12-17', '2018-12-24',
               '2018-12-31'],
              dtype='datetime64[ns]', freq='W-MON')

shift 연산

시계열 데이터의 인덱스는 시간이나 날짜를 나타내기 때문에 날짜 이동 등의 다양한 연산이 가능하다. 예를 들어 shift연산을 사용하면 인덱스는 그대로 두고 데이터만 이동할 수도 있다.

np.random.seed(0)
ts = pd.Series(np.random.randn(4), index=pd.date_range("2018-1-1", periods=4, freq="M"))
ts
2018-01-31    1.764052
2018-02-28    0.400157
2018-03-31    0.978738
2018-04-30    2.240893
Freq: M, dtype: float64
ts.shift(1)
2018-01-31         NaN
2018-02-28    1.764052
2018-03-31    0.400157
2018-04-30    0.978738
Freq: M, dtype: float64
ts.shift(-1)
2018-01-31    0.400157
2018-02-28    0.978738
2018-03-31    2.240893
2018-04-30         NaN
Freq: M, dtype: float64
ts.shift(1, freq="M")
2018-02-28    1.764052
2018-03-31    0.400157
2018-04-30    0.978738
2018-05-31    2.240893
Freq: M, dtype: float64
ts.shift(1, freq="W")
2018-02-04    1.764052
2018-03-04    0.400157
2018-04-01    0.978738
2018-05-06    2.240893
dtype: float64

resample 연산

resample 연산을 쓰면 시간 간격을 재조정하는 리샘플링(resampling)이 가능하다. 이 때 시간 구간이 작아지면 데이터 양이 증가한다고 해서 업-샘플링(up-sampling)이라 하고 시간 구간이 커지면 데이터 양이 감소한다고 해서 다운-샘플링(down-sampling)이라 부른다.

ts = pd.Series(np.random.randn(100), index=pd.date_range("2018-1-1", periods=100, freq="D"))
ts.tail(20)
2018-03-22    0.625231
2018-03-23   -1.602058
2018-03-24   -1.104383
2018-03-25    0.052165
2018-03-26   -0.739563
2018-03-27    1.543015
2018-03-28   -1.292857
2018-03-29    0.267051
2018-03-30   -0.039283
2018-03-31   -1.168093
2018-04-01    0.523277
2018-04-02   -0.171546
2018-04-03    0.771791
2018-04-04    0.823504
2018-04-05    2.163236
2018-04-06    1.336528
2018-04-07   -0.369182
2018-04-08   -0.239379
2018-04-09    1.099660
2018-04-10    0.655264
Freq: D, dtype: float64

다운-샘플링의 경우에는 원래의 데이터가 그룹으로 묶이기 때문에 그룹바이(groupby)때와 같이 그룹 연산을 해서 대표값을 구해야 한다.

ts.resample('W').mean()
2018-01-07    0.697206
2018-01-14    0.468797
2018-01-21    0.249052
2018-01-28    0.301938
2018-02-04    0.023198
2018-02-11    0.283486
2018-02-18   -0.096136
2018-02-25   -0.446708
2018-03-04    0.155312
2018-03-11    0.200799
2018-03-18   -0.376808
2018-03-25   -0.895860
2018-04-01   -0.129493
2018-04-08    0.616422
2018-04-15    0.877462
Freq: W-SUN, dtype: float64
ts.resample('M').first()
2018-01-31   -1.173123
2018-02-28    0.676433
2018-03-31    0.087551
2018-04-30    0.523277
Freq: M, dtype: float64

날짜가 아닌 시/분 단위에서는 구간위 왼쪽 한계값(가장 빠른 값)은 포함하고 오른쪽 한계값(가장 늦은 값)은 포함하지 않는다. 즉, 가장 늦은 값은 다음 구간에 포함된다. 예를 들어 10분 간경으로 구간을 만들면 10의 배수가 되는 시각은 구간의 시작점이 된다.

ts = pd.Series(np.random.randn(60), index=pd.date_range("2018-1-1", periods=60, freq="T"))
ts.head(20)
2018-01-01 00:00:00    2.259309
2018-01-01 00:01:00   -0.042257
2018-01-01 00:02:00   -0.955945
2018-01-01 00:03:00   -0.345982
2018-01-01 00:04:00   -0.463596
2018-01-01 00:05:00    0.481481
2018-01-01 00:06:00   -1.540797
2018-01-01 00:07:00    0.063262
2018-01-01 00:08:00    0.156507
2018-01-01 00:09:00    0.232181
2018-01-01 00:10:00   -0.597316
2018-01-01 00:11:00   -0.237922
2018-01-01 00:12:00   -1.424061
2018-01-01 00:13:00   -0.493320
2018-01-01 00:14:00   -0.542861
2018-01-01 00:15:00    0.416050
2018-01-01 00:16:00   -1.156182
2018-01-01 00:17:00    0.781198
2018-01-01 00:18:00    1.494485
2018-01-01 00:19:00   -2.069985
Freq: T, dtype: float64
ts.resample('10T').sum()
2018-01-01 00:00:00   -0.155837
2018-01-01 00:10:00   -3.829915
2018-01-01 00:20:00   -0.115280
2018-01-01 00:30:00   -3.234560
2018-01-01 00:40:00   -4.452304
2018-01-01 00:50:00   -0.906751
Freq: 10T, dtype: float64

왼쪽이 아니라 오른쪽 한계값을 구간에 포함하려면 closed="right" 인수를 사용한다. 이 때는 10의 배수가 되는 시각이 앞 구간에 포함된다.

ts.resample('10T', closed="right").sum()
2017-12-31 23:50:00    2.259309
2018-01-01 00:00:00   -3.012462
2018-01-01 00:10:00   -2.806340
2018-01-01 00:20:00   -1.354903
2018-01-01 00:30:00   -4.004135
2018-01-01 00:40:00   -3.180252
2018-01-01 00:50:00   -0.595865
Freq: 10T, dtype: float64

ohlc 매서드는 구간의 시고저종(open, high, low, close)값을 구한다.

ts.resample('5T').ohlc()
open high low close
2018-01-01 00:00:00 2.259309 2.259309 -0.955945 -0.463596
2018-01-01 00:05:00 0.481481 0.481481 -1.540797 0.232181
2018-01-01 00:10:00 -0.597316 -0.237922 -1.424061 -0.542861
2018-01-01 00:15:00 0.416050 1.494485 -2.069985 -2.069985
2018-01-01 00:20:00 0.426259 0.676908 -0.637437 -0.132881
2018-01-01 00:25:00 -0.297791 1.152332 -1.676004 1.079619
2018-01-01 00:30:00 -0.813364 0.521065 -1.466424 0.141953
2018-01-01 00:35:00 -0.319328 0.694749 -1.383364 -1.383364
2018-01-01 00:40:00 -1.582938 0.610379 -1.582938 -0.596314
2018-01-01 00:45:00 -0.052567 0.523891 -1.936280 0.088422
2018-01-01 00:50:00 -0.310886 1.955912 -2.772593 1.955912
2018-01-01 00:55:00 0.390093 0.493742 -0.652409 -0.116104

업-샘플링의 경우에는 실제로 존재하지 않는 데이터를 만들어야 한다. 이 때는 앞에서 나온 데이터를 뒤에서 그대로 쓰는 forward filling 방식과 뒤에서 나올 데이터를 앞에서 미리 쓰는 backward filling 방식을 사용할 수 있다. 각각 ffill, bfill 매서드를 이용한다.

ts.resample('30s').ffill().head(20)
2018-01-01 00:00:00    2.259309
2018-01-01 00:00:30    2.259309
2018-01-01 00:01:00   -0.042257
2018-01-01 00:01:30   -0.042257
2018-01-01 00:02:00   -0.955945
2018-01-01 00:02:30   -0.955945
2018-01-01 00:03:00   -0.345982
2018-01-01 00:03:30   -0.345982
2018-01-01 00:04:00   -0.463596
2018-01-01 00:04:30   -0.463596
2018-01-01 00:05:00    0.481481
2018-01-01 00:05:30    0.481481
2018-01-01 00:06:00   -1.540797
2018-01-01 00:06:30   -1.540797
2018-01-01 00:07:00    0.063262
2018-01-01 00:07:30    0.063262
2018-01-01 00:08:00    0.156507
2018-01-01 00:08:30    0.156507
2018-01-01 00:09:00    0.232181
2018-01-01 00:09:30    0.232181
Freq: 30S, dtype: float64

dt 접근자

datetime 자료형 시리즈에는 dt접근자가 있어 datetime 자료형이 가진 몇가지 유용한 속성과 매서드를 사용할 수 있다.

s = pd.Series(pd.date_range("2020-12-25", periods=100, freq="D"))
s
0    2020-12-25
1    2020-12-26
2    2020-12-27
3    2020-12-28
4    2020-12-29
        ...    
95   2021-03-30
96   2021-03-31
97   2021-04-01
98   2021-04-02
99   2021-04-03
Length: 100, dtype: datetime64[ns]

예를 들어 year, month, day, weekday 등의 속성을 이용하면 년, 월, 일, 요일 정보를 빼낼 수 있다.

s.dt.year
0     2020
1     2020
2     2020
3     2020
4     2020
      ... 
95    2021
96    2021
97    2021
98    2021
99    2021
Length: 100, dtype: int64
s.dt.weekday
0     4
1     5
2     6
3     0
4     1
     ..
95    1
96    2
97    3
98    4
99    5
Length: 100, dtype: int64

strftime 매서드를 이용하여 문자열을 만드는 것도 가능하다.

s.dt.strftime("%Y년 %m월 %d일")
0     2020년 12월 25일
1     2020년 12월 26일
2     2020년 12월 27일
3     2020년 12월 28일
4     2020년 12월 29일
          ...      
95    2021년 03월 30일
96    2021년 03월 31일
97    2021년 04월 01일
98    2021년 04월 02일
99    2021년 04월 03일
Length: 100, dtype: object

Quiz, 다음 명령으로 만들어진 데이터프레임에 대해 월별 value의 합계를 구하라. (힌트 : groupby 매서드와 dt 접근자를 사용))

np.random.seed(0)
df = pd.DataFrame({
    "date": pd.date_range("2020-12-25", periods=100, freq="D"), 
    "value": np.random.randint(100, size=(100,))
})

출처 : 데이터사이언스 스쿨(http://datascienceschool.net)

@winuss
Hello :) Developer notes!