시계열

시계열time series (2)

nova-unum 2022. 7. 29. 14:40

시계열time series (2)

시계열기초

● 색인, 선택, 부분 선택

● 중복된 색인을 갖는 시계열

● 날짜 범위, 빈도, 이동

날짜 범위, 빈도, 이동

● 날짜 범위 생성하기

● 빈도와 날짜 오프셋

● 데이터 시프트


pandas 에서 찾아볼 수 있는 가장 기본적인 시계열 객체의 종류는 파이썬 문자열이나 datetime객체로 표현되는 타임스탬프로 색인된 Seriese다.

from datetime import datetime

dates =[datetime(2011, 1, 2), datetime(2011, 1, 5),
        datetime(2011, 1, 7), datetime(2011, 1, 8),
        datetime(2011, 1, 10), datetime(2011, 1, 12)]

ts = pd.Series(np.random.randn(6), index=dates)

ts
2011-01-02    0.750074
2011-01-05   -0.059697
2011-01-07    1.177863
2011-01-08   -0.964222
2011-01-10   -1.474337
2011-01-12    1.879105

내부적으로 보면 이들 datetime 객체는 DatetimeIndex에 들어 있으며 ts 변수의 타입은 TimeSeries다.

ts.index

DatetimeIndex(['2011-01-02', '2011-01-05', '2011-01-07', '2011-01-08',
               '2011-01-10', '2011-01-12'],
              dtype='datetime64[ns]', freq=None)

다른 Series와 마찬가지로 서로 다르게 색인된 시계열 객체 간의 산술 연산은 자동으로 날짜에 맞춰진다

ts + ts[::2]

2011-01-02    1.500148
2011-01-05         NaN
2011-01-07    2.355726
2011-01-08         NaN
2011-01-10   -2.948674
2011-01-12         NaN
dtype: float64

 

pandas는 Numpy의 datetime64 자료형을 사용해서 나노초의 정밀도를 가지는 타임스탬프를 저장한다.

ts.index.dtype
dtype('<M8[ns]')

DatetimeIndex의 스칼라값은 pandas의 Timestamp 객체다.

stamp = ts.index[0]
stamp

Timestamp('2011-01-02 00:00:00')

Timestamp는 datetime 객체를 사용하는 어떤 곳에도 대체 사용이 가능하다. 게다가 가능하다면 빈도에 관한 정보도 저장하며 시간대 변환을 하는 방법과 다른 종류의 조작을 하는 방법도 포함하고 있다.

색인, 선택, 부분 선택

시계열은 라벨에 기반해서 데이터를 선택하고 인덱싱 할 때 pandas.Series와 동일하게 동작한다.

stamp = ts.index[2]

ts[stamp]

1.1778631236389685

해석할 수 있는 날짜를 문자열로 넘겨서 편리하게 사용할 수 있다.

ts['1/10/2011']
-1.4743368507036818

ts['20110110']
-1.4743368507036818

긴 시계열에서는 연을 넘기거나 연, 월말 넘겨서 데이터의 일부 구간만 선택할 수도 있다.

longer_ts = pd.Series(np.random.randn(1000), index=pd.date_range('1/1/2000', periods=1000))
longer_ts
2000-01-01    0.676988
2000-01-02   -0.563301
2000-01-03    0.173741
2000-01-04    2.258907
2000-01-05    0.317499
                ...   
2002-09-22   -0.895426
2002-09-23   -1.732854
2002-09-24    0.409506
2002-09-25   -0.609459
2002-09-26    0.280980
Freq: D, Length: 1000, dtype: float64

longer_ts['2001']
2001-01-01    0.552069
2001-01-02   -1.021433
2001-01-03    0.444831
2001-01-04   -0.484344
2001-01-05   -1.227576
                ...   
2001-12-27   -0.870527
2001-12-28    0.454767
2001-12-29    1.107678
2001-12-30    0.631664
2001-12-31   -0.423717
Freq: D, Length: 365, dtype: float64

longer_ts['2001-05']
2001-05-01    1.317174
2001-05-02   -0.862651
2001-05-03   -1.389682
2001-05-04    0.871189
2001-05-05    0.545053
2001-05-06    1.196764
2001-05-07   -0.207974
2001-05-08   -0.065703
2001-05-09   -1.337239
2001-05-10    0.371315
2001-05-11   -1.768673
2001-05-12   -2.243905
2001-05-13   -1.490849
2001-05-14    0.716593
2001-05-15   -0.420931
2001-05-16    1.128196
2001-05-17   -0.440174
2001-05-18   -0.838870
2001-05-19   -0.474804
2001-05-20    0.217053
2001-05-21   -1.655350
2001-05-22   -1.359490
2001-05-23    1.281644
2001-05-24   -0.820742
2001-05-25   -1.091330
2001-05-26    0.328021
2001-05-27   -0.090328
2001-05-28    1.472031
2001-05-29   -0.273797
2001-05-30   -0.720485
2001-05-31   -0.310048
Freq: D, dtype: float64

datetime 객체로 데이터를 잘라내는 작업은 일반적인 Series와 동일한 방식으로 할 수 있다.

ts[datetime(2011, 1, 7):]
2011-01-07    1.177863
2011-01-08   -0.964222
2011-01-10   -1.474337
2011-01-12    1.879105
dtype: float64

대부분의 시계열 데이터는 연대순으로 정렬되기 때문에 범위를 지정하기 위해 시계열에 포함하지 않고 타임스탬프를 시용해서 Series를 나눌수 있다. 

ts['1/6/2011':'1/11/2011']
2011-01-07    1.177863
2011-01-08   -0.964222
2011-01-10   -1.474337
dtype: float64

날짜 문자열이나 datetime 혹은 타임스탬프를 넘길 수 있다. 이런 방식으로 데이터를 나누면 NumPy 배열을 나누는 것처럼 원본 시계열에 대한 뷰를 생성한다는 사실을 기억하자. 즉, 데이터 복사가 발생하지 않고 슬라이스에 대한 변경이 원본 데이터에도 반영된다.

이와 동일한 인스턴스 메서드로 truncate가 있는데, 이 메서드는 TimeSeries를 두 개의 날짜로 나눈다.

ts.truncate(after='1/9/2011')
2011-01-02    0.750074
2011-01-05   -0.059697
2011-01-07    1.177863
2011-01-08   -0.964222
dtype: float64

위 방식은 DataFrame 에서도 동일하게 적용되며 로우에 인덱싱된다.

dates = pd.date_range('1/1/2000', periods=100, freq='W-WED')
long_df = pd.DataFrame(np.random.randn(100,4),
                      index = dates, columns=['Colorado','Texas','New York', 'Ohio'])
                   
long_df.loc['5-2001']

wn

중복된 색인을 갖는 시계열

어떤 애플리케이션에서는 여러 데이터가  특정 타임스탬프에 몰려 있는 것을 발견할 수 있다.

dates = pd.DatetimeIndex(['1/1/2000','1/2/2000','1/2/2000','1/2/2000','1/3/2000'])
dup_ts = pd.Series(np.arange(5), index=dates)
dup_ts
2000-01-01    0
2000-01-02    1
2000-01-02    2
2000-01-02    3
2000-01-03    4
dtype: int32

is_unique 속성을 통해 확인해보면 색인이 유일하지 않음을 알 수 있다.

dup_ts.index.is_unique
False

이 시계열 데이터를 인덱싱하면 타임스탬프의 중복 여부에 따라 스카라값이나 슬라이스가 생성된다.

dup_ts['1/3/2000'] # 중복 없음
4

dup_ts['1/2/2000'] #중복 있음
2000-01-02    1
2000-01-02    2
2000-01-02    3
dtype: int32

유일하지 않은 타임스탬프를 가지는 데이터를 집계한다고 해보자. 한 가지 방법은 groupby에 level=0(단일 단계 인덱싱)을 넘기는 것이다.

grouped = dup_ts.groupby(level=0)
grouped.mean()
2000-01-01    0.0
2000-01-02    2.0
2000-01-03    4.0
dtype: float64

grouped.count()
2000-01-01    1
2000-01-02    3
2000-01-03    1
dtype: int64

날짜 범위, 빈도, 이동

pandas에서 일반적인 시계열은 불규칙적인 것으로 간주된다. 즉, 고정된 빈도를 갖지 않는다. pandas에는 리샘플링, 표준 시계열 빈도 모음, 빈도 추론 그리고 고정된 빈도의 날짜 범위를 위한 도구가 있다.

아래 예제 시계열을 고정된 일 빈도로 변환하려면 resample 메서드를 사용하면 된다.

resampler = ts.resample('D')

문자열 'D'는 일 빈도로 해석된다.

날짜 범위 생성하기

pandas.date_range를 사용하면 특정 빈도에 따라 지정한 길이만큼의 DatetimeIndex를 생성한다

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

기본적으로 date_range는 일별 타임스탬프를 생성한다. 만약 시작 날짜나 종료 날짜만 넘긴다면 생성할 기간의 숫자를 함게 전달해야 한다.

pd.date_range(start='2012-04-01', periods=20)
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
               '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08',
               '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12',
               '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16',
               '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20'],
              dtype='datetime64[ns]', freq='D')

 

pd.date_range(end='2012-06-01', periods=20)
DatetimeIndex(['2012-05-13', '2012-05-14', '2012-05-15', '2012-05-16',
               '2012-05-17', '2012-05-18', '2012-05-19', '2012-05-20',
               '2012-05-21', '2012-05-22', '2012-05-23', '2012-05-24',
               '2012-05-25', '2012-05-26', '2012-05-27', '2012-05-28',
               '2012-05-29', '2012-05-30', '2012-05-31', '2012-06-01'],
              dtype='datetime64[ns]', freq='D')

시작과 종료 날짜는 생성된 날짜 색인에 대해 엄격한 경계를 정의한다. 예를 들어 날짜 색인이 각 월의 마지막 영업일을 포함하도록 하고 싶다면 빈도값으로 'BM' (월 영어마감일)을 전달할 것이다.

pd.date_range('2000-01-01','2000-12-01',freq = 'BM')
DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31', '2000-04-28',
               '2000-05-31', '2000-06-30', '2000-07-31', '2000-08-31',
               '2000-09-29', '2000-10-31', '2000-11-30'],
              dtype='datetime64[ns]', freq='BM')
​

 

기본 시계열 빈도

축약 오프셋종류 설명 축약 오프셋종류 설명
D Day 달력상의 일 B BusinessDay 매 영업일
H Hour 매시 T 또는 min Minute 매분
S Second 매초 L 또는 ms Milli 밀리초(1/1000초)
U Micro 마이크로초(1/1,000,000초) M MonthEnd 월 마지막 일
BM BusinessMonthEnd 월 영업마감일 MS MonthBegin 월 시작일
BMS BusinessMonthBegin 월 영업시작일 W-Mon, W-TUE, ... Week 요일, MON, TUE, WED, THU, FRI, SAT, SUN
WOM-1MON, WOM-2MON WeekOfMonth 월별 주차와 요일. 예를 들어 WOM-3FRI는 매월 3째주 금요일이다.
Q-JAN, Q-FEB QuarterEnd 지정된 월을 해당년도의 마감으로 하며 지정된 월의 마지막 날짜를 가리키는 분기 주기(JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC)
BQ-JAN, BQ-FEB,... BusinessQuarterEnd 지정된 월을 해당년도의 마감으로 하며 지정된 월의 마지막 영업일을 가리키는 분기 주기
QS-JAN, QS-FEB, .... QuarterBegin 지정된 월을 해당년도의 마감으로 하며 지정된 월의 첫 번째 날을 가리키는 분기 주기
BQS-JAN, BSQ-FEB, .... BusinessQuarterBegin 지정된 월을 해당년도의 마감으로 하며 지정된 월의 첫 번째 영업일을 가리키는 분기 주기
 A-JAN, A-FEB YearEnd 주어진 월의 마지막 일을 가리키는 연간 주기(JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC)
BA-JAN, BA-FEB,. BusinessYearEnd 주어진 월의 영업 마감일을 가리키는 연간 주기
AS-JAN, AS-FEB,. YearBegin 주어진 월의 시작일을 가리키는 연간 주기
BAS-JAN, BAS-FEB, ... BusinessYearBegin 주어진 월의 영업 시작일을 가리키는 연간 주기

date_range는 기본적으로 시작 시간이나 종료 시간의 타임스탬프(존재한다면)를 보존한다.

pd.date_range('2012-05-02 12:56:31', periods=5)
DatetimeIndex(['2012-05-02 12:56:31', '2012-05-03 12:56:31',
               '2012-05-04 12:56:31', '2012-05-05 12:56:31',
               '2012-05-06 12:56:31'],
              dtype='datetime64[ns]', freq='D')

가끔은 시간 정보를 포함하여 시작 날짜와 종료 날짜를 갖고 있으나 관례에 따라 자정에 맞추어 타임스탬프를 정규화하고 싶을 때가 있다. 이렇게 하려면 normalize 옵션을 사용한다.

pd.date_range('2012-05-02 12:56:31', periods=5, normalize=True)
DatetimeIndex(['2012-05-02', '2012-05-03', '2012-05-04', '2012-05-05',
               '2012-05-06'],
              dtype='datetime64[ns]', freq='D')

빈도와 날짜 오프셋

pandas에서 빈도는 기본 빈도 base frequency와 배수의 조합으로 이루어진다. 기본 빈도는 보통  'M'(월별), 'H'(시간별) 처럼 짧은 문자열로 참조된다. 각 기본 빈도에는 일반적으로 날짜 오프셋 date offset 이라고 불리는 객체를 사용할 수 있다. 예를 들어 시간별 빈도는 Hour 클래스를 사용해서 표현할 수 있다. 

from pandas.tseries.offsets import Hour, Minute
hour = Hour()
hour
<Hour>

이 오프셋의 곱은 정수를 넘겨서 구할 수 있다

four_hours = Hour(4)
four_hours
<4 * Hours>

대부분의 애플리케이션에서는 이런 객체들을 직접 만들어야 할 경우는 절대 없겠지만 대신 'H' 또는 '4H'처럼 문자열로 표현하게 될 것이다. 기본 빈도 앞에 정수를 두면 해당 빈도의 곱을 생성한다.

pd.date_range('2000-01-01','2000-01-03 23:59', freq='4h')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 04:00:00',
               '2000-01-01 08:00:00', '2000-01-01 12:00:00',
               '2000-01-01 16:00:00', '2000-01-01 20:00:00',
               '2000-01-02 00:00:00', '2000-01-02 04:00:00',
               '2000-01-02 08:00:00', '2000-01-02 12:00:00',
               '2000-01-02 16:00:00', '2000-01-02 20:00:00',
               '2000-01-03 00:00:00', '2000-01-03 04:00:00',
               '2000-01-03 08:00:00', '2000-01-03 12:00:00',
               '2000-01-03 16:00:00', '2000-01-03 20:00:00'],
              dtype='datetime64[ns]', freq='4H')

여러 오프셋을 덧셈으로 합칠 수 있다.

Hour(2) + Minute(30)
<150 * Minutes>

유사하게 빈도 문자열로 '1h30min' 을 넘겨도 같은 표현으로 잘 해석된다.

pd.date_range('2000-01-01', periods=10, freq='1h30min')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:30:00',
               '2000-01-01 03:00:00', '2000-01-01 04:30:00',
               '2000-01-01 06:00:00', '2000-01-01 07:30:00',
               '2000-01-01 09:00:00', '2000-01-01 10:30:00',
               '2000-01-01 12:00:00', '2000-01-01 13:30:00'],
              dtype='datetime64[ns]', freq='90T')

어떤 빈도는 시간상에서 균일하게 자리 잡고 있지 않은 경우도 있다. 예를 들어 'M'(월 마지막일)은 월중 일수에 의존적이며 'BM'(월 영업마감일)은 월말이 주말인지 아닌지에 따라 다르다. 

월별 주차

한 가지 유용한 빈도 클래스는 WOM 으로 시작하는 '월별 주차'다. 월별 주차를 사용하면 매월3째주 금요일 같은 날짜를 얻을 수 있다.

rng = pd.date_range('2012-01-01','2012-09-01', freq='WOM-3FRI')
list(rng)
[Timestamp('2012-01-20 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-02-17 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-03-16 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-04-20 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-05-18 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-06-15 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-07-20 00:00:00', freq='WOM-3FRI'),
 Timestamp('2012-08-17 00:00:00', freq='WOM-3FRI')]

 

데이터 시프트 

시프트는 데이터를 시간 축에서 앞이나 뒤로 이동하는 것을 의미한다. Series와 DataFrame은 색인은 변경하지 않고 데이터를 앞이나 뒤로 느슨한 시프트를 수행하는  shift 메서드를 가지고 있다.

ts = pd.Series(np.random.randn(4), index=pd.date_range('1/1/2000', periods=4, freq='M'))
ts
2000-01-31    0.468811
2000-02-29   -1.476559
2000-03-31   -1.711522
2000-04-30    0.287189
Freq: M, dtype: float64

ts.shift(2)
2000-01-31         NaN
2000-02-29         NaN
2000-03-31    0.468811
2000-04-30   -1.476559
Freq: M, dtype: float64

ts.shift(-2)
2000-01-31   -1.711522
2000-02-29    0.287189
2000-03-31         NaN
2000-04-30         NaN
Freq: M, dtype: float64

시프트를 하게 되면 시계열의 시작이나 끝에 결측치가 발생하게 된다.

shift는 일반적으로 한 시계열 내에서 , 혹은 DataFrame의 컬럼으로 표현할 수 있는 여러 시계열에서의 퍼센트 변화를 계산할 때 흔히 사용한다. 

ts / ts.shift(1) -1

느슨한 시프트는 색인을 바꾸지 않기 때문에 어떤 데이터는 버려지기도 한다. 그래서 만약 빈도를 알고 있다면 shift에 빈도를 넘겨서 타임스탬프가 확장되도록 할 수 있다. 

ts.shift(2, freq='M')
2000-03-31    0.468811
2000-04-30   -1.476559
2000-05-31   -1.711522
2000-06-30    0.287189
Freq: M, dtype: float64
ts.shift(3, freq='D')
2000-02-03    0.468811
2000-03-03   -1.476559
2000-04-03   -1.711522
2000-05-03    0.287189
dtype: float64

ts.shift(1, freq='90T')
2000-01-31 01:30:00    0.468811
2000-02-29 01:30:00   -1.476559
2000-03-31 01:30:00   -1.711522
2000-04-30 01:30:00    0.287189
dtype: float64

여기서 T는 분을 나타낸다.

오프셋만큼 날짜 시프트하기

pandas의 날짜 오프셋은 datetime이나 timestamp 객체에서도 사용할 수 있다.

from pandas.tseries.offsets import Day, MonthEnd
now = datetime(2011, 11, 17)
now + 3 * Day()
Timestamp('2011-11-20 00:00:00')

만일 MonthEnd 같은 앵커드 오프셋을 추가한다면 빈도 규칙의 다음 날짜로 롤 포워드 roll forward된다.

now + MonthEnd()
Timestamp('2011-11-30 00:00:00')

now + MonthEnd(2)
Timestamp('2011-12-31 00:00:00')

앵커드 오프셋은 rollforward와 rollback 메서드를 사용해서 명시적으로 각각 날짜를 앞으로 밀거나 뒤로 당길 수 있다.

offset = MonthEnd()

offset.rollforward(now)
Timestamp('2011-11-30 00:00:00')

offset.rollback(now)
Timestamp('2011-10-31 00:00:00')

이 메서드를 groupby와 함께 사용하면 날짜 오프셋을 영리하게 사용할 수 있다.

ts = pd.Series(np.random.randn(20), 
              index=pd.date_range('1/15/2000', periods=20, freq='4d'))
              
ts.groupby(offset.rollforward).mean()
2000-01-31   -0.423390
2000-02-29    0.102703
2000-03-31   -0.044605
dtype: float64

물론 가장 쉽고 빠른 방법은 resample을 사용하는 것이다.

ts.resample('M').mean()
2000-01-31   -0.423390
2000-02-29    0.102703
2000-03-31   -0.044605
Freq: M, dtype: float64