3월 말부터 AI, 빅데이터 관련 공부를 시작했고 벌써 약 두 달 가량 공부를 했다.
아직 뭘 한건지 모르겠는 부분도 있고, 많이 배웠다 싶은 부분도 있고, 정신이 없지만 일단 배운걸 좀 기록해 봐야겠다는 생각에
당장 학원에서 진행중인 데이콘 1등 솔루션을 가지고 책과 코드에 나와있는 내용을 대략적으로 따라가면서, 이렇게 하면 어떨까, 혹은 사고 과정이 어떻게 됐을까 하는 것들을 좀 적어보려고 한다.
제일 먼저 작성 할 것은 당장 우리 조에서 맡게 된 상점 신용카드 매출 예측이고 개요는 아래와 같다.
・ 주 최 : 펀다(FUNDA), 데이콘
・ 문 제 : '16. 6. 1. ~ '19. 2. 28. 까지의 매출 데이터를 기반으로 '19. 3. 1 ~ 5. 31까지 3개월간의 상점별 총 매출을 예측할 것
・ 평가 척도 : MAE (Mean Absolute Error, 평균절대오차)
・ 기 간 : '19. 7. 11. ~ 8. 31.
・ 참여 팀 : 292팀
관련 자료는 아래 주소에서 받아 볼 수 있다.
・ 소스코드 : https://github.com/wikibook/dacon.git
・ 데이터 : https://dacon.io/competitions/official/140472/data
거두절미하고 이제 시작하자.
import pandas as pd
import numpy as np
import seaborn as sns
from tqdm import tqdm
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
import os
os.chdir('/Users/heechankang/projects/pythonworkspace/dacon_data/credit_card')
이제 시작하자마자 당연스럽게 불러오는 각종 라이브러리들과 경로지정.
특히 세 번째 chdir를 통한 경로지정이 정말 효자다.
왜인지는 모르지만 위처럼 별도로 지정을 안해주면 항상 경로가 매번 이상한곳으로 바뀌어있어서 파일을 다룰때 골치가 아팠다.
특히나 파일을 불러올때는 에러나고 끝이지만 저장할때 이상한곳에 숨어있어서 꼭 한번 더 찾아줘야 했는데
이걸 알게 된 뒤로 그럴 일이 없어서 참 좋다.
그리고 잡다한 경고문구를 막아줘서 모니터를 깔끔하게 해 주는 warings와, 이번에 처음 쓰기시작한 tqdm.
tqdm은 각종 작업들의 예상 종료시간을 시각화해주는 라이브러리인데, 뭐 이게 있다고 빨라지거나 하는 건 아니지만
뭔가 마음에 안정감을 준다고 할까? 그래도 대충 언제쯤 끝나겠거니 하는게 참 중간에 딴짓도 할 수 있게 해 주고 좋다.
train = pd.read_csv('funda_train.csv')
train.head()
train.info()
train.describe()
train.shape
계속해서 파일을 불러와 주고, 양교수님의 말씀대로 습관적으로 대략적인 형태를 훑어보기 시작한다.
자료에 포함된 변수명을 정리 해 보면 아래와 같이 총 9개의 변수로 이루어져있고, 몇가지 NaN 값도 눈에 띈다.
・ store_id : 상점명(코드)
・ card_id : 사용된 카드의 아이디
・ card_company : 카드회사
・ transacted_date : 거래일자
・ transacted_time : 거래시간
・ installment_term : 할부 개월
・ region : 지역
・ type_of_business : 업종
・ amont : 매출액
와,, 자료는 0번부터 6,556,612까지 약 650만개 이상의 행으로 구성되어있고 csv파일 주제에 450메가가 넘는다.
일단 당장 눈에 보이는 건 저 object 변수들. 특히 그중에서도 date 와 time이 좀 거슬려서 가공 해 줄 필요가 있어보인다.
음 자료 특성상 크게 영양가 없어 보이는 표이다. store_id와 card_id의 평균값 등은 전혀 쓸모없는 자료이고, installment_term도 크게 유용할것 같지는 않다.
그런데 특이한게 하나 눈에 띄는데, 매출액에 음수가 있다는 것이다. 음 매출에 음수,, 솔직히 난 처음에 이게 뭘까 한참 생각했다.
위와 같이 대략적으로 자료의 형태를 살펴보면서 두 가지 사실을 알아냈다.
첫째는 자료에 많든 적든 결측치가 존재 한다는 점이고, 둘째는 매출에 음수가 존재한다는 것이다.
이어서 계속 진행하면,
plt.figure(figsize = (13, 4))
plt.bar(train.columns, train.isnull().sum())
plt.xticks(rotation = 45)
역시 결측치는 눈에 띄는대로 없애버릴 준비를 한다. plt.figure로 틀을 만들어주고 bar 그래프에서 x축에는 columns를 넣어주고 train.isnull().sum()을 통해 결측치의 개수를 구해 y값으로 넣어준다.
x축의 라벨이 겹치지 않게 45도 회전도 시켜줬다. 이걸 자동으로 해주는 코드가 있었던 것 같은데, 찾아봐야겠다.
아래는 위 코드의 결과이다.
이건 솔직히, 보고 좀 의아했다. 이런 결측치가 왜 생길까. 거래량 600만건 중에서 400만건이 업종도 모르는 상점에서 거래됐다니.
지역도 거의 1/3가량은 모른다. 흠.. 이런걸 쓰라고 준것도 좀 신기하네. 뭐 내가 모르는 분야니까 넘어가자.
뭐 당연한 게, 이게 뭐 정량데이터도 아니고 채울 수 있는 방법이 없다. 업종을 다 치킨집으로 바꾼다면 그래도 좀 맞을까? 하는 쓸데없는 생각도 잠깐 해봤다.
어찌됐든 결측치가 있다는 건 참을 수 없으므로 어떻게든 없애야 한다. 여기서 우승자분께서는 과감히 drop처리.
train = train.drop(['region', 'type_of_business'], axis = 1)
train.head()
그리고 바로 이어서 아까 두번째 문제점인, 음수 amount에 대해서 처리방안을 강구한다. 일단 한번 생긴걸 좀 보자.
plt.figure(figsize=(8, 4))
sns.boxplot(train['amount'])
여기서 또 궁금증이 생겼다. 일단 박스플롯이 이따위로 생겼다는 말은,, 평균적으로 분포하는 값의 분산은 매우 작고, 이상치로 판단될 만 한 요소가 엄청나게 크고, 많다는 이야기겠지. 처음엔 뭐가 잘못 나온 줄 알았다.
그리고 이따위로 생긴 상자수염의 정체를 이해하고 난 뒤에 또다른 궁금증은, 이걸 굳이 왜 박스플롯으로 그렸나? 하는 것이었는데, 직접 코드로 써넣어보니 당장 변수 하나만 가지고 그려 볼 수 있는 제일 만만한 차트가 박스플롯이라는 걸 깨달았다.
train[train['amount']<0].head()
train[train['amount']<0].count()
그래서 직접 코드로 써서 확인. 참고로 아랫줄은 그냥 내가 궁금해서 써넣은 코드이다.
이런 음수 데이터가 73,100개 있다고 한다. 그리고 이런 음수를 없애주기 위해 우승자분께서는 아래와 같은 코드를 작성했다.
# 거래일과 거래시간을 합친 변수 생성
train['datetime'] = pd.to_datetime(train.transacted_date + ' '+
train.transacted_time, format = '%Y-%m-%d %H:%M:%S')
# 환불 거래를 제거하는 함수 정의
def remove_refund(df):
refund = df[df['amount']<0] # 매출액 음숫값 데이터를 추출
non_refund = df[df['amount']>0] # 매출액 양숫값 데이터를 추출
removed_data = pd.DataFrame()
for i in tqdm(df.store_id.unique()):
# 매출액이 양숫값인 데이터를 상점별로 나누기
divided_data = non_refund[non_refund['store_id']==i]
# 매출액이 음숫값인 데이터를 상점별로 나누기
divided_data2 = refund[refund['store_id']==i]
for neg in divided_data2.to_records()[:]: # 환불 데이터를 차례대로 검사합니다.
refund_store = neg['store_id']
refund_id = neg['card_id'] # 환불 카드 아이디를 추출
refund_datetime = neg['datetime'] # 환불 시간을 추출
refund_amount = abs(neg['amount']) # 매출 음숫값의 절댓값을 구합니다.
# 환불 시간 이전의 데이터 중 카드 아이디와 환불액이 같은 후보리스트 뽑기
refund_pay_list = divided_data[divided_data['datetime']<=refund_datetime]
refund_pay_list = refund_pay_list[refund_pay_list['card_id']==refund_id]
refund_pay_list = refund_pay_list[refund_pay_list['amount']==refund_amount]
# 후보 리스트가 있으면 카드 아이디, 환불액이 같으면서 가장 최근시간을 제거
if len(refund_pay_list) != 0:
# 가장 최근시간 구하기
refund_datetime = max(refund_pay_list['datetime'])
# 가장 최근 시간
noise_list = divided_data[divided_data['datetime']==refund_datetime]
# 환불 카드 아이디
noise_list = noise_list[noise_list['card_id']==refund_id]
# 환불액
noise_list = noise_list[noise_list['amount']==refund_amount]
# 인덱스를 통해 제거
divided_data = divided_data.drop(index = noise_list.index)
# 제거한 데이터를 데이터프레임에 추가
removed_data = pd.concat([removed_data, divided_data], axis = 0)
return removed_data
이게 참, 주석으로 달자니 헷갈리고, 덩어리별로 잘라서 넣자니 번잡해서 그냥 아래에 쭉 해설을 달아야겠다.
먼저 간단한 쿼리로 매출액이 각각 음수와 양수인 그룹으로 자료를 분할했다. 여기에 추가적으로 반환해줄 removed_data를 미리 만들어줬다.
이후 store_id.unique()를 통해서 각 상점들의 이름 고유값을 가지고 각각의 상점에 대해 for문을 돌려서 양수와 음수를 각 divided_data와 divided_data2에 담아준다.
여기서 작성자는 2중 for문으로 음수 데이터인 divided_data2에 대해, to_records()[:]함수를 통해 각 행을 numpy array로 바꿔줌으로써 일종의 dictionary처럼 활용했고, 각 행의 store_id, card_id, datetime, amount 데이터를 뽑아냈다. 아,, 근데 datetime이라는 column은 없는데..? 뭐지....
아 맨 위에서 transacted_date 와 time을 가지고 to_datetime 함수로 train['datetime'] column을 추가로 만들어줬다. 이부분을 빼먹을 뻔 했네.
여튼 이어서 뽑아낸 자료를 통해, 음수 매출의 경우 가장 가까운 이전시간대의 매출 중 card+id와 절댓값이 같은 자료와 함께 없애버린다.
positive_data = remove_refund(train)
plt.figure(figsize=(8, 4))
sns.boxplot(positive_data['amount'])
위에서 만든 함수 활용. 참 계속 보면서 느끼는거지만 이분은 함수활용을 참 잘하시는 것 같다.
그런데 6백만개짜리 자료를 이중 for문으로 돌리니까 참,, 시간이,, 꽤 걸린다. 이럴 때 tqdm이 정말 효자다.
바로 이어서 처리 후 boxplot도 보여준다. 역시 음수 값이 모두 사라졌다.
positive_data.head()
음수값이 제거된 이후의 데이터 형태. 사실 head만 찍어서는 뭐가 달라진건지 안보인다. 오히려 아까 NaN 없앤 게 보일 뿐.
이어서 train 자료에 대해서 다운샘플링을 진행한다.
사유는, 현재 데이터는 위에서 보다시피 분단위까지 기입되어있다. 다시 말 해서, 쓸 데 없이 자세하게 나와있다. 이는 예측 할 구간의 수가 매우 커진다는 뜻이며, 이는 정확도도 떨어지고 연산의 부담도 커진다는 뜻으로, 전혀 좋을 게 없다. 고로 우승자분 께서는 이 간격을 월단위로 재조정하는 과정을 거쳤다.
# 월 단위 다운샘플링 함수 정의
def month_resampling(df):
new_data = pd.DataFrame()
# 연도와 월을 합친 변수를 생성
df['year_month'] = df['transacted_date'].str.slice(stop = 7)
# 데이터의 전체 기간을 추출
year_month = df['year_month'].drop_duplicates()
# 상점 아이디별로 월 단위 매출액 총합 구하기
downsampling_data = df.groupby(['store_id', 'year_month']).amount.sum()
downsampling_data = pd.DataFrame(downsampling_data)
downsampling_data = downsampling_data.reset_index(drop = False, inplace = False)
for i in tqdm(df.store_id.unique()):
# 상점별로 데이터 처리
store = downsampling_data[downsampling_data['store_id']==i]
# 각 상점의 처음 매출이 발생한 월을 구하기
start_time = min(store['year_month'])
# 모든 상점을 전체 기간 데이터로 만들기
store = store.merge(year_month, how = 'outer')
# 데이터를 시간순으로 정렬
store = store.sort_values(by = ['year_month'], axis = 0, ascending = True)
# 매출이 발생하지 않는 월은 2로 채우기
store['amount'] = store['amount'].fillna(2)
# 상점 아이디 결측치 채우기
store['store_id'] = store['store_id'].fillna(i)
# 처음 매출이 발생한 월 이후만 뽑기
store = store[store['year_month']>=start_time]
new_data = pd.concat([new_data, store], axis = 0)
return new_data
이분은 여기서도 함수를 정의해서 활용했다. 음,, 왜 이렇게까지 하셨는지는 모르겠다. 어찌됐건, 위 함수를 통해 train 데이터셋에 'year_month'라는 column을 추가해주고, 월까지 기입한다.
이후 store_id 와 year_month를 기준으로 데이터를 합산해서 downsampling_data 변수에 저장하여 데이터프레임으로 만들어준다.
그 외에도 눈에 띄는 부분이 몇 가지 있는데, 먼저 다운샘플링하며 난장판이 된 index를 재설정해주는 것과, 매출이 없는 월을 숫자 2로 채워넣은 것이다. index야 그렇다지만 2는 뭘까 했는데, 이후 로그정규화를 염두에 두고, 로그로 인해 값이 튀는것을 방지하기 위함이라고 한다. 대단.
하지만 여전히, 이걸 굳이 왜 함수로 만든건지는 잘 모르겠다.
일단 어찌됐든, 함수를 실행 해 준다.
resampling_data = month_resampling(positive_data)
resampling_data['store_id'] = resampling_data['store_id'].astype(int)
resampling_data
음,, 여기에 이어서 DataFrame을 Series로 바꿔주는 함수도 정의한다.
def time_series(df, i):
# 상점별로 데이터 뽑기
store = df[df['store_id']==i]
# 날짜 지정 범위는 영어 시작 월부터 2019년 3월 전 영업마감일까지.
index = pd.date_range(min(store['year_month']), '2019-03', freq = 'BM')
# 시리즈 객체로 변환
ts = pd.Series(store['amount'].values, index = index)
return ts
뭐, 간단하다. 주어진 df에서 i라는 store_id를 가진 데이터를 뽑아내어 인덱스를 다시 달아준다. 이 때 freq = 'BM'을 통해 마지막 영업일의 날짜를 반환해준다. 그 형태는 아래와 같다.
일단 오늘은 여기까지 포스팅 해야겠다.
이어서 탐색적 데이터 분석과 모델링, 향상방안까지 이어서 포스팅을 해보도록 노력하겠다.