데이터 한 그릇

1) 시계열 데이터의 발견 및 다루기 본문

시계열 분석/Practical TIme Series Analysis

1) 시계열 데이터의 발견 및 다루기

장사이언스 2022. 10. 6. 19:28
  •  
  • 온라인 레포에서 시계열 데이터를 찾는 방법
  • 시계열을 고려하지 않고 수집된 데이터에서 시계열 데이터를 발견하고 준비하는 방법
  • 시계열 데이터를 다룰 때 나타나는 일반적인 난제, 특히 타임스팸프가 초래하는 어려움을 다루는 방법

시계열 데이터는 어디서 찾는가?

 

  • 미리 준비된 데이터 셋
  • 발견된 시계열

 

미리 준비된 데이터 셋

  • UCI 머신러닝 저장소
  • UEA 및 UCR 시계열 분류 저장소
  • 정부 시계열 데이터셋

 

  • CompEngine
  • R 패키지: Mcomp 와 M4comp2018

 

발견된 시계열

 

  • 타임스탬프가 어디에나 존재할 수 있다는 관점에서 보면, 시계열이라고 명시되지 않은 구조화된 데이터에서 시계열 데이터를 찾는 것은 쉬운 일
    • ex) 회사의 하루 총 거래량, 여성 고객의 주당 지출 총액, 다변량 시계열 데이터도 생성도 가능 (18세 미만 모든 고객의 주당 지출 총액, 65세 이상의 여성 고객의 주당 지출 총액, 회사가 광고에 쓴 주당 지출 총액을 개별적으로 나타낼 수 있는, 각 시간 단계에서 세 가지 지표가 제공되므로 다변량 시계열로 볼 수 있음)

타임스탬프가 찍힌 이벤트 기록

 

  • 파일에 접근한 시간을 기록하기만 해도 시계열을 구성할 수 있음
  • 임의 시점과 그 시점을 기준으로 이후의 시점의 타임스탬프로 시간의 변화량을 모델링하여 시간에 대한 시간축과 시간 변화량에 대 한 값축으로 시계열 구성 가능

 

시간을 대체하는 '시간이 없는' 측정

 

  • 데이터셋의 숨은 논리로 시간이 설명되는 경우

 

물리적 흔적

 

  • 의학, 청각학, 기상학 등과 같은 많은 과학 학분 분야에서 물리적 흔적을 기록

 

테이블 집합에서 시계열 데이터 집합 계산하기(중요)

 

시계열 데이터 집합 조립하기

  • 세 개의 table 가정
회원ID 가입 연도 회원 상태
1 2017 gold
2 2018 silver
3 2016 inactive
회원ID 열어본 이메일 개수
2 2017-01-08 3
2 2017-01-15 2
1 2017-01-15 1

 

회원ID 타임스탬프 기부금
2 2017-05-22 11:27:49 1,000
2 2017-04-13 09:19:02 350
1 2017-01-01 00:15:45 25

 

시계열 데이터를 합쳐서 어떤 분석을 해보고 싶을 수 있다. 그렇다면 가장 먼저 해야할건 시간축을 선택해야한다

예를 들어서 이메일 개수와 기부의 상관관계를 밝히고 싶은 연구가 있다고 했을 때, 두 개의 시간축 중에 어떤 시간축으로 결정하여 데이터 프레임을 합치는게 좋을지 생각해야한다

 

그리고 데이터가 의미하는 것과 독자가 생각한 의미가 일치하는지 생각해야한다

회원의 가입연도와 회원의 상태에 관한 데이터 프레임을 살펴보자

데이터 프레임은 가입연도에 따른 회원의 등급이 나타나 있다

이때 회원의 등급은 가입했을 당시의 등급일 가능성도 있고, 가입한 이후 현재의 등급 상태일 수 있다

이렇듯 값이 언제 할당되었는지 알 수 없는 상태 변수의 사용은 지양해야한다

 

만일 과거 데이터 분석에 현재 상태를 적용하게 되면 그땐, 알 수 없는 시계열 모델에 무언가를 입력해주기 때문에 사전관찰(look-ahead) 로 볼 수 있다

 

사전 관찰이란?

시계열 분석에서 사전관찰이란 용어는 미래의 어떤 사실을 안다는 뜻으로 사용
사전관찰은 데이터를 통해 실제로 알아야 하는 시점보다 더 일찍 미래에 대한 사실을 발견하는 방법

한 시점에 대해 다음에 일어날 일을 아는 모델을 선택한다면 문제가 발생할 수 있다

 

그 이후 시계열 데이터가 어떤  단위(년,주,일)로 나뉘어져 있다면 단위 정의에 대해서 따져봐야한다

신기하게도, 주 단위로 나뉘어져 있다면 이 과정은 더욱 중요한 과정으로 취급된다

만일 사람의 활동을 분석한다면 한 주를 사람의 활동 주기와 유사하도록 일요일부터 토요일 또는 월요일부터 일요일로 정하는 게 좋다

(어떤 데아터는 수요일 ~화요일 단위로 정리할 수도 있다)

 

다음으로는 Null 값에 대해서 고민해봐야 한다

이메일 데이터에 주목했을 때, 어떤 회원이 단 한 번도 이메일을 열람하지 않은 주가 있는지 파악하는 것과 같다

Null 값이 비어있는 정보기 때문에 쓸모없는 게 아니라 Null 그 자체가 의미를 포함하고 있기 때문에 중요하다

 

밑으로는 파이썬 실습이 이루어지는데 글쓴이 미친놈이 데이터를 공유해놓지 않아서 코드 논리로 내용을 읽어나가야 한다

 

##python

emails[emails.EmailsOpened < 1] #이메일 연 횟수가 한 번도 없는놈 데려오자

 

결과는 Empty DataFrame 이 도출된다

직관적으로 모든 회원이 단 한 번도 매주 빠지지 않고 최소 한 번씩은 이메일을 열어봤다는건 우리 입장에서 설명력이 매우 떨어짐을 알 수 있다

 

따라서, 한 회원에 대해서 살펴봄으로써 위의 사실이 맞는 사실인지 틀린 사실인지 살펴봐야한다

 

emails[emails.member == 998] #998 회원의 정보를 전부 가져오자

 

위의 998번 회원에 대한 output을 살펴보면 "week" 라는 칼럼에 주 단위로 데이터가 정리되어 있는걸 확인할 수 있는데, 1주일 단위로 데이터가 저장되어 있다가 2017-12-18 ~ 2018-01-01 데이터 사이의 공백을 발견할 수 있다. 즉 일부 주(week) 가 누락됨을 확인할 수 있다

이메일 데이터임을 고려할 때 이는 998번 회원이 한 번도 이메일을 열람하지 않은 주가 있음을 의미한다

 

 

그렇다면 특정 회원에 대한 사건(이메일을 여는 것)이 처음 일어난 날과 마지막에 일어난 날을 구한 이후에 뺄셈을 한 이후 7로나누면 총 몇주의 데이터를 본래(?) 가지고 있어야 하는지 알 수 있다

 

(max(emails[emails.member == 998].week) - min(emails[emails.member == 998].week)).days / 7

 

위의 코드대로 돌리게 되면 output으로 25를 얻을 수 있고 결국 26주를 본래(?) 가지고 있어야 함을 알 수 있다

지금까진 회원별로 누락된 주(null) 값을 확인하는 법을 살펴봤다

 

다음으로는 누락된 값을 채워넣는 방식을 살펴보자

사건이 처음 기록된 날짜 이전이나 사건이 마지막에 기록된 날짜 이후에도 사건이 발생할 수 있기 때문에 모든 누락된 null 값을 채우는건 쉽지 않다

따라서 null 값이 아닌 사건이 기록된 회원의 데이터 중에서 처음과 마지막 시기의 사잇값으로 누락된 부분을 채울 수 있다

 

complete_idx = pd.MultiIndex.from_product((set(emails.week),set(emails.member)))

 

MultiIndex 는 다중 index 를 정할 수 있게 해준다

complete_idx 변수에 unique 한 week 값과 member 값을 인덱스로 저장해놓는다

(이 값들은 후에 인덱스로 활용하려고 한다)

 

#재색인 메서드: reindex
all_email = emails.set_index(['week','member']).reindex(complete_idx, fill_value = 0).reset_index()

#재설정된 색인에 의해 생성된 열의 이름을 붙여준다
all_email.columns = ['week','member','EmailsOpened']

 

위의 테이블에 있는 email 테이블에서 "week" 와 "member" 를 인덱스로 지정해준다

reindex 로 위에서 정의한 complete_idx 를 해주면 unique 한 날짜 값과 회원 값으로 인덱스를 지정하게 된다

index 를 새로 지정했기 때문에 발생하는 null 값은 fill_value 를 0으로 주어 0으로 채운다

(새로운 인덱스의 값이 있으면 그대로 값을 유지하지만, 새로운 인덱스 값이 들어갔을 때 값이 없다면 0으로 채우게 된다, 따라서 unique 한 날짜에 값이 없는 곳은 null 이 채워지게 된다)

 

참조

 

이제 null 값을 채워줬으면 다시 998번 회원의 정보를 확인할 수 있다

 

all_emails[all_email.member == 998].sort_values('week')
 
 
 
위의 코드를 실행하면 998번 회원의 정보를 확인할 수 있는데 초기에 0으로 대다수가 채워지게 된다
이는 회원 가입 이전이기 때문에 이메일 수신 대상자가 아니므로 생기는 0값이라고 할 수 있다
(unique 값으로 reindex 하여 fill_value를 0으로 준걸 생각해보자)
 
 
어떤 분석이든 최초의 사건 몇 주 전과 같은 데이터를 널로 유지하는 분석방법은 없다
따라서 회원의 최초 이메일 수신 시점을 발견하여 최초 이메일 수신 이전을 삭제해야만 한다
 
 
cutoff_dates = emails.groupby('member').week.agg(['min','max']).reset_index()
cutoff_dates = cutoff_dates.reset_index()

 

위의 코드는, email 테이블을 회원별로 그룹화하여 집계함수를 사용해 회원별 최초 이메일 수신 시점과 마지막 수신 시점을 발견하는 코드다

for _, row in cutoff_dates.iterrows():
  member = row['member']
  start_date = row['min']
  end_date = row['max']
  
  #start_date 이전의 주에 대한 내용 삭제
  all_email.drop(all_email[all_email.member == member][all_email.week < start_date].index, inplace = True)
  
  
  #end_date 이후의 주에 대한 내용을 삭제
  all_email.drop(all_email[all_email.member == member][all_email.week > end_date].index, inplace = True)

 

 

여기까지 시계열 데이터 분석에서 null 값을 제거하는 예시를 살펴봤따

이제 시계열 분석을 한다고 했을 때 시계열 데이터 프레임끼리 merge 하는 과정을 살펴보도록 하자

 

위의 테이블들 예시에서 주 단위로 기부금 데이터를 취합하면 이메일 데이터와 기간 단위의 비교가 가능해지고, 이메을 응답과 기부가 어떤 식으로 연관이 있는지에 대해 합리적으로 조사할 수 있다

그리고 전 주의 열람된 이메일의 개수를 주어진 주의 기부금 예측 변수로 취급할 수있다

 

여기서 중요한점은 시계열 단위가 동일하다는 가정하에 분석이 진행된다는 점이다

 

donations.timestamp = pd.to_datetime(donations.timestamp)
donation.set_index('timestamp', inplace = True)
agg_don = donations.groupby('member').apply(lambda df: df.amount.resample("W-MOM").sum().dropna())

 

코드를 살펴보면 기부금에 관한 테이블의 인덱스를 timestamp로 설정한 이후에 멤버별 기부금 주간 총액을 단위를 맞춰서 agg_don에 저장한걸 볼 수있다

resample 함수는 datetime 데이터에 대해 특정 단위로 재배열해주는 함수다 (datetime index를 원하는 주기로 나눠주는 메서드)

주 단위를 월요일 기준으로 맞추기 위해서 .resample("W-MOM") 을 사용해 주었으며, 회원별 정의한 주 단위별 총 기부액을 알고싶기 때문에 sum() 을 해준다

기부가 없는 주는 dropna로 제거해준다

 

 

참조

 

donation 을 시계열 단위를 맞춰서 전처리가 됐으면 email 데이터 프레임과 합쳐주면 된다

 

for member, member_email in all_email.groupby('member'):

  ##특정 회원의 기부 데이터 추출

  member_donations = agg_donations[agg_donations.member == member]

  ##기부 데이터의 색인을 timestamp로 설정

  member_donations.set_index('timestamp', inplace = True)
  member_email.set_index('week', inplace = True)

  member_email = all_email[all_email.member == member]
  member_email.sort_values('week').set_index('week')
  df = pd.merge(member_email, member_donations, how = 'left',
                left_index = True,
                right_index = True)
  

  df.fillna(0)

  df['member'] = df.member_x
  merged_df = merged_df.append(df.reset_index()[['member','week','emailsOpened','amount']])

 

특정 회원으로 두 데이터를 맞추고 인덱스도 맞춘 이후에 member_email 인덱스 기준으로 두 데이터를  left join 해준다

두 데이터 프레임이 합쳐질 때 member_email 의 인덱스가 member_donation 의 인덱스에 없을 경우 null 값으로 채워지게 되고

해당 널값을 0으로 채워준다 (2017-01-15 가 member_email 의 인덱스인데 member_donation 에 없는 경우 합쳐진 데이터 프레임은 해당 자리에 null 값을 반환한다, 즉 해당 날짜에 dona가 없었다는걸 의미하고 이를 0으로 채워준다)

 

 

 

 

참조

 

이렇게 합쳐진 데이터를 그대로 기부 관련한 상태 변수로 사용할 수 있지만, 그대로 사용하게 된다면 앞서 살펴봤던 사전관찰 문제가 발생하게 된다

따라서 기부금액을 한 주 뒤로 미뤄야한다

 

df = merged_df[merged_df.member == 998]

df['target'] = df.amount.shift(1)

df = df.fillna(0)
df

 

 

타임스탬프의 문제점

 

  • 발생 사건에 대한 기록은 실제와 일치하지 않을 수 있다
    • 어떤 사람이 노트를 작성하고 csv 파일로 옮겼다고 했을 때, 이 기록에 대한 timestamp는 노트를 작성한 시점인지, csv파일로 옮긴 시점인지 고려되어야 한다 (즉, 메타데이터가 timestamp를 이해할때 굉장히 중요하다는 것)
    • timestamp는 어느 나라 시간인가?
    • 어플의 경우 오프라인에서 발생한 사건 로그가 온라인일 때 전송될 때 timestamp를 찍을 수 있다. (실제 시간과의 오차 발생)

 

타임스탬프를 추측하여 데이터 이해하기

 

  • 하지만 timestamp에 대한 메타 데이터를 얻기 힘들 수 있다

 

몇 가지 고려해볼 지점

 

  1. timestamp는 현지 시간을 의미하는가 세계 표준 시간을 의미하는가?
  2. 시간이 사용자의 행동을 반영하는가 연결과 같은 외부 제약을 반영하는가?

 

현지 시간과 세계 표준 시간

 

대부분의 timestamp는 협정 세계시로 저장이 되기 때문에 현지 시간으로 저장되는 경우는 없지만 모든 경우를 고려해봐야한다

만일 timestamp를 기준으로 사람의 하루 패턴이 보인다면 이는 현지 시간을 기준으로 저장됐을 가능성이 크다

시간 단위로 먹은 음식을 기록한 앱이 있을 때, 음식 기록이 현저히 적은 시간은 밤으로 간주할 수 있으며 이는 현지 시간으로 timestamp가 저장됐음을 알 수 있다

Comments