4.8 시계열 자료 다루기

DatetimeIndex 인덱스

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

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

  • pd.to_datetime 함수

  • pd.date_range 함수

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

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)

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

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.date_range 함수를 쓰면 모든 날짜/시간을 일일히 입력할 필요없이 시작일과 종료일 또는 시작일과 기간을 입력하면 범위 내의 인덱스를 생성해 준다.

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')
pd.date_range("2018-4-1", "2018-12-31", freq="MS")
DatetimeIndex(['2018-04-01', '2018-05-01', '2018-06-01', '2018-07-01',
               '2018-08-01', '2018-09-01', '2018-10-01', '2018-11-01',
               '2018-12-01'],
              dtype='datetime64[ns]', freq='MS')
pd.date_range("2018-4-1", "2018-12-31", freq="M")
DatetimeIndex(['2018-04-30', '2018-05-31', '2018-06-30', '2018-07-31',
               '2018-08-31', '2018-09-30', '2018-10-31', '2018-11-30',
               '2018-12-31'],
              dtype='datetime64[ns]', freq='M')
pd.date_range("2018-4-1", "2018-12-31", freq="BMS")
DatetimeIndex(['2018-04-02', '2018-05-01', '2018-06-01', '2018-07-02',
               '2018-08-01', '2018-09-03', '2018-10-01', '2018-11-01',
               '2018-12-03'],
              dtype='datetime64[ns]', freq='BMS')
pd.date_range("2018-4-1", "2018-12-31", freq="BM")
DatetimeIndex(['2018-04-30', '2018-05-31', '2018-06-29', '2018-07-31',
               '2018-08-31', '2018-09-28', '2018-10-31', '2018-11-30',
               '2018-12-31'],
              dtype='datetime64[ns]', freq='BM')
pd.date_range("2018-1-1", "2018-12-31", freq="WOM-2THU")
DatetimeIndex(['2018-01-11', '2018-02-08', '2018-03-08', '2018-04-12',
               '2018-05-10', '2018-06-14', '2018-07-12', '2018-08-09',
               '2018-09-13', '2018-10-11', '2018-11-08', '2018-12-13'],
              dtype='datetime64[ns]', freq='WOM-2THU')
pd.date_range("2018-1-1", "2018-12-31", freq="Q-JAN")
DatetimeIndex(['2018-01-31', '2018-04-30', '2018-07-31', '2018-10-31'], dtype='datetime64[ns]', freq='Q-JAN')
pd.date_range("2018-1-1", "2018-12-31", freq="Q-DEC")
DatetimeIndex(['2018-03-31', '2018-06-30', '2018-09-30', '2018-12-31'], dtype='datetime64[ns]', freq='Q-DEC')

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    1.488252
2018-03-23    1.895889
2018-03-24    1.178780
2018-03-25   -0.179925
2018-03-26   -1.070753
2018-03-27    1.054452
2018-03-28   -0.403177
2018-03-29    1.222445
2018-03-30    0.208275
2018-03-31    0.976639
2018-04-01    0.356366
2018-04-02    0.706573
2018-04-03    0.010500
2018-04-04    1.785870
2018-04-05    0.126912
2018-04-06    0.401989
2018-04-07    1.883151
2018-04-08   -1.347759
2018-04-09   -1.270485
2018-04-10    0.969397
Freq: D, dtype: float64

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

ts.resample('W').mean()
2018-01-07    0.305776
2018-01-14    0.629064
2018-01-21   -0.006910
2018-01-28    0.277065
2018-02-04   -0.144972
2018-02-11   -0.496299
2018-02-18   -0.474473
2018-02-25   -0.201222
2018-03-04   -0.775142
2018-03-11    0.052868
2018-03-18   -0.450379
2018-03-25    0.601892
2018-04-01    0.334893
2018-04-08    0.509605
2018-04-15   -0.150544
Freq: W-SUN, dtype: float64
ts.resample('M').first()
2018-01-31    1.867558
2018-02-28    0.156349
2018-03-31   -1.726283
2018-04-30    0.356366
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   -1.173123
2018-01-01 00:01:00    1.943621
2018-01-01 00:02:00   -0.413619
2018-01-01 00:03:00   -0.747455
2018-01-01 00:04:00    1.922942
2018-01-01 00:05:00    1.480515
2018-01-01 00:06:00    1.867559
2018-01-01 00:07:00    0.906045
2018-01-01 00:08:00   -0.861226
2018-01-01 00:09:00    1.910065
2018-01-01 00:10:00   -0.268003
2018-01-01 00:11:00    0.802456
2018-01-01 00:12:00    0.947252
2018-01-01 00:13:00   -0.155010
2018-01-01 00:14:00    0.614079
2018-01-01 00:15:00    0.922207
2018-01-01 00:16:00    0.376426
2018-01-01 00:17:00   -1.099401
2018-01-01 00:18:00    0.298238
2018-01-01 00:19:00    1.326386
Freq: T, dtype: float64
ts.resample('10T').sum()
2018-01-01 00:00:00    6.835324
2018-01-01 00:10:00    3.764630
2018-01-01 00:20:00    0.776495
2018-01-01 00:30:00   -0.538336
2018-01-01 00:40:00    1.828234
2018-01-01 00:50:00    0.167957
Freq: 10T, dtype: float64

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

ts.resample('10T', closed="right").sum()
2017-12-31 23:50:00   -1.173123
2018-01-01 00:00:00    7.740444
2018-01-01 00:10:00    3.338065
2018-01-01 00:20:00    0.835217
2018-01-01 00:30:00    2.480654
2018-01-01 00:40:00   -0.653363
2018-01-01 00:50:00    0.266409
Freq: 10T, dtype: float64

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

ts.resample('5T').ohlc()
open high low close
2018-01-01 00:00:00 -1.173123 1.943621 -1.173123 1.922942
2018-01-01 00:05:00 1.480515 1.910065 -0.861226 1.910065
2018-01-01 00:10:00 -0.268003 0.947252 -0.268003 0.614079
2018-01-01 00:15:00 0.922207 1.326386 -1.099401 1.326386
2018-01-01 00:20:00 -0.694568 1.849264 -0.694568 0.672295
2018-01-01 00:25:00 0.407462 0.539249 -0.769916 0.031831
2018-01-01 00:30:00 -0.635846 0.676433 -0.635846 0.396007
2018-01-01 00:35:00 -1.093062 0.635031 -1.491258 0.635031
2018-01-01 00:40:00 2.383145 2.383145 -1.315907 -1.315907
2018-01-01 00:45:00 -0.461585 1.713343 -0.826439 -0.826439
2018-01-01 00:50:00 -0.098453 1.126636 -1.147469 -1.147469
2018-01-01 00:55:00 -0.437820 1.929532 -0.498032 0.087551

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

ts.resample('30s').ffill().head(20)
2018-01-01 00:00:00   -1.173123
2018-01-01 00:00:30   -1.173123
2018-01-01 00:01:00    1.943621
2018-01-01 00:01:30    1.943621
2018-01-01 00:02:00   -0.413619
2018-01-01 00:02:30   -0.413619
2018-01-01 00:03:00   -0.747455
2018-01-01 00:03:30   -0.747455
2018-01-01 00:04:00    1.922942
2018-01-01 00:04:30    1.922942
2018-01-01 00:05:00    1.480515
2018-01-01 00:05:30    1.480515
2018-01-01 00:06:00    1.867559
2018-01-01 00:06:30    1.867559
2018-01-01 00:07:00    0.906045
2018-01-01 00:07:30    0.906045
2018-01-01 00:08:00   -0.861226
2018-01-01 00:08:30   -0.861226
2018-01-01 00:09:00    1.910065
2018-01-01 00:09:30    1.910065
Freq: 30S, dtype: float64
ts.resample('30s').bfill().head(20)
2018-01-01 00:00:00   -1.173123
2018-01-01 00:00:30    1.943621
2018-01-01 00:01:00    1.943621
2018-01-01 00:01:30   -0.413619
2018-01-01 00:02:00   -0.413619
2018-01-01 00:02:30   -0.747455
2018-01-01 00:03:00   -0.747455
2018-01-01 00:03:30    1.922942
2018-01-01 00:04:00    1.922942
2018-01-01 00:04:30    1.480515
2018-01-01 00:05:00    1.480515
2018-01-01 00:05:30    1.867559
2018-01-01 00:06:00    1.867559
2018-01-01 00:06:30    0.906045
2018-01-01 00:07:00    0.906045
2018-01-01 00:07:30   -0.861226
2018-01-01 00:08:00   -0.861226
2018-01-01 00:08:30    1.910065
2018-01-01 00:09:00    1.910065
2018-01-01 00:09:30   -0.268003
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

연습 문제 4.8.1

다음 명령으로 만들어진 데이터프레임에 대해 월별 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,))
})