[머신러닝/딥러닝] 향후 판매량 예측 (2)_베이스라인 모델
<머신러닝. 딥러닝 문제해결 전략> 2부 9장 실습하고 해당 내용을 정리한 내용입니다.
https://www.kaggle.com/competitions/competitive-data-science-predict-future-sales
Predict Future Sales | Kaggle
www.kaggle.com
1. 베이스라인 모델 설계
데이터를 적절히 처리하여, LightGBM을 사용하여 베이스라인 모델을 만들겠습니다.
데이터 불러오기 | |
(기본적인) 피처 엔지니어링 | 1. 피처명 한글화 2. 데이터 다운캐스팅 3. 데이터 조합 생성 4. 타깃값 피처 추가 |
평가지표 계산 함수 작성 | RMSE (사이킷런 제공) |
모델훈련 | 모델 : LightGBM (사이킷런 제공) |
성능 검증 | |
제출 |
먼저 데이터부터 불러옵니다.
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings(action='ignore') # 경고 문구 생략
# 데이터 경로
data_path = '/kaggle/input/competitive-data-science-predict-future-sales/'
sales_train = pd.read_csv(data_path + 'sales_train.csv')
shops = pd.read_csv(data_path + 'shops.csv')
items = pd.read_csv(data_path + 'items.csv')
item_categories = pd.read_csv(data_path + 'item_categories.csv')
test = pd.read_csv(data_path + 'test.csv')
submission = pd.read_csv(data_path + 'sample_submission.csv')
이전의 코드와 다르게 추가된 것만 보겠습니다.
warnings.filterwarnings(action='ignore') # 경고 문구 생략
LightGBM으로 범주형 데이터를 모델링하면, 모델링에는 지장이 없지만 불필요한 경고 문구가 뜹니다. 위의 코드는 경고 문구를 생략해주는 역할을 합니다.
2. 피처 엔지니어링 Ⅰ : 피처명 한글화
본 경진대회는 훈련 데이터를 여러 파일로 제공하고 피처도 다양합니다. 그러니 피처명이 영어면 헷갈릴 수 있습니다. 이때 피처를 쉽게 알아보려면 피처명이 한글인 것이 좋습니다. 따라 sales_train, shops, items, item_categories, test 데이터의 피처명을 모두 한글로 바꿔주겠습니다.
가장 먼저 sales_train 피처명을 한글로 바꾸겠습니다.
sales_train = sales_train.rename(columns={'date': '날짜',
'date_block_num': '월ID',
'shop_id': '상점ID',
'item_id': '상품ID',
'item_price': '판매가',
'item_cnt_day': '판매량'})
sales_train.head()
위와 같이 sales_train의 date, date_block_num, shop_id, item_id, item_price, item_cnt_day 피처가 각각 날짜, 월ID, 상점ID, 상품ID, 판매가, 판매량으로 잘바뀐 것을 확인 할 수 있습니다.
나머지 shops, items, item_categories, test 피처명도 동일하게 모두 한글로 바꿔주겠습니다.
shops = shops.rename(columns={'shop_name': '상점명',
'shop_id': '상점ID'})
shops.head()
items = items.rename(columns={'item_name': '상품명',
'item_id': '상품ID',
'item_category_id': '상품분류ID'})
items.head()
item_categories = item_categories.rename(columns=
{'item_category_name': '상품분류명',
'item_category_id': '상품분류ID'})
item_categories.head()
test = test.rename(columns={'shop_id': '상점ID',
'item_id': '상품ID'})
test.head()
이상으로 모든 데이터의 피처명을 한글로 바꿨습니다. 이로 피처를 한눈에 파악하기 한결 수월해졌습니다!
2. 피처 엔지니어링 Ⅱ : 데이터 다운캐스팅
다운캐스팅(downcasting)이란 더 작은 데이터 타입으로 변환하는 작업을 말합니다. 이번에는 데이터 다운캐스팅을 왜 하는지와 그 방법을 배워보겠습니다.
예를 들면 설명하겠습니다. 만약에 꽃 한송이를 키운다고 했을 때, 작은 화분에서 키우는 것이 좋습니다. 한 송이를 키운다고 광활한 들판에서 한 송이만 키운다면 이는 엄청난 낭비입니다. 이와 마찬가지로 데이터가 작은데 큰 데이터 타입을 사용하면 메모리를 낭비하게 됩니다. 주어진 데이터 크기에 딱 맞는 타입을 사용하는 것이 좋습니다.
판다스로 데이터를 불러오면 기본적으로 정수형은 int64, 실수형은 float64 타입으로 할당합니다. 이러한 타입은 각각의 타입에서 가장 큰 타입에 해당합니다. 모든 피처가 최대 타입을 사용할 필요는 없습니다. 크지 않은 숫자가 저장된 피처라면, int8 ,in16, int32, float8, float16, float32 등 보다 작은 타입으로 할당할 수 있습니다. 그래야만 메모리 낭비를 막고, 훈련 속도도 빠르게 진행할 수 있습니다.
이렇게 해당 피처 크기에 맞게 적절한 타입으로 바꿔주는 함수인 downcast()를 사용하도록 하겠습니다.
def downcast(df, verbose=True):
start_mem = df.memory_usage().sum() / 1024**2
for col in df.columns:
dtype_name = df[col].dtype.name
if dtype_name == 'object':
pass
elif dtype_name == 'bool':
df[col] = df[col].astype('int8')
elif dtype_name.startswith('int') or (df[col].round() == df[col]).all():
df[col] = pd.to_numeric(df[col], downcast='integer')
else:
df[col] = pd.to_numeric(df[col], downcast='float')
end_mem = df.memory_usage().sum() / 1024**2
if verbose:
print('{:.1f}% 압축됨'.format(100 * (start_mem - end_mem) / start_mem))
return df
이 함수를 이용해 shops, item_categories, items, sales_train, test에 데이터 다운캐스팅을 하겠습니다.
다운캐스팅 결과 메모리 사용량이 크게 줄어든 것을 볼 수 있습니다.
3. 피처 엔지니어링 Ⅲ : 데이터 조합 생성
테스트 데이터 피처는 ID 피처를 제외하면 상점ID, 상품ID 피처입니다. 우리가 예측해야하는 값은 각 상점의 상품별 월간 판매량이였습니다. 그렇기 때문에 월, 상점, 상품별 조합이 필요합니다. 조합을 만든다는 의미를 쉽게 파악해보겠습니다.
다음 그림을 참고하시면 됩니다.
원본 데이터의 월ID, 상점ID, 상품ID 피처가 왼쪽과 같다고 합시다.
①월 ID가 0일 때 상점ID는 0, 상품ID는 5와 10이 있습니다.
②월ID가 1일 때 상점ID는 0과 1, 상품ID는 5와 10이 있습니다.
③월ID가 2일 때 상점ID는 0과 1, 상품ID는 5와 10이 있습니다.
월ID별로 한 번이라도 동일한 상점ID, 상품ID가 있다면 그것들의 조합을 만듭니다. 그리하여 월ID, 상점ID, 상품ID를 조합을 오른쪽과 같이 만드는 것입니다. 이때 노란색에 해당하는 부분들이 추가되었습니다.
월ID가 1일때, 상점ID는 0, 상품ID는 5인 경우는 존재하지 않습니다. 따라 판매량이 0인채 추가되었습니다.
이러한 과정이 의미가 없어 보일 수도 있지만, 판매량이 0이더라도 의미있는 데이터는 많으면 많을수록 좋기에 이러한 과정이 필요합니다.
지금까지 설명한 데이터 조합을 만들어보겠습니다. 데이터 조합은 itertools가 제공하는 product()함수로 쉽게 만들 수 있습니다.
from itertools import product
train = []
# ① 월ID, 상점ID, 상품ID 조합 생성
for i in sales_train['월ID'].unique():
all_shop = sales_train.loc[sales_train['월ID']==i, '상점ID'].unique()
all_item = sales_train.loc[sales_train['월ID']==i, '상품ID'].unique()
train.append(np.array(list(product([i], all_shop, all_item))))
idx_features = ['월ID', '상점ID', '상품ID'] # 기준 피처
# ② 리스트 타입인 train을 DataFrame 타입으로 변환
train = pd.DataFrame(np.vstack(train), columns=idx_features)
train
①이 월ID, 상점ID, 상품ID 피처 조합을 만드는 코드입니다. 월ID의 고윳값(0~33)별로 모든 상점ID 고윳값, 상품ID 고윳값을 구해 조합을 생성합니다. 코드 실행 후에 train은 34개 배열을 원소로 갖게 됩니다.
②는 train 내 34개의 배열을 하나로 합쳐 DataFrame을 만드는 코드입니다.
조합이 잘 생성된 것을 확인할 수 있습니다.
위에 보이는 것처럼 sales_train의 데이터 개수는 2,935,849개 였습니다. 조합 생성 후 10,913,850개로 3.7배 정도 늘어났습니다.
참고로 이렇게 만든 train을 앞으로 훈련 데이터의 뼈대로 사용합니다. 뼈대가 되는 train에 타깃값을 병합하고, 나머지 shops, items, items_categories도 병합할 예정입니다.
4. 피처 엔지니어링 Ⅳ : 타깃값(월간 판매량) 추가
이제부터 train에 다른 데이터도 추가할 것입니다. 처음에 추가할 것은 타깃값인 각 상점의 상품별 월간 판매량입니다.
현재 sales_train에는 일별 판매량을 나타내는 '판매량' 피처가 있습니다. 하지만 우리가 원하는 타깃값은 각 상점의 상품별 '월간' 판매량 입니다. 이 값을 구하기 위해서는 월ID, 상점ID, 상품ID를 기준으로 그룹화해 판매량을 더해야 합니다. groupby()함수를 활용하면 됩니다. 앞서 월ID, 상점ID, 상품ID 조합을 만들 때, ['월ID', '상점ID', '상품ID' ]를 idx_features 변수에 할당했습니다. 이 변수를 기준으로 그룹화해서 각 상점의 상품별 월간 판매량을 구해보겠습니다.
# idx_features를 기준으로 그룹화해 판매량 합 구하기
group = sales_train.groupby(idx_features).agg({'판매량': 'sum'})
# 인덱스 재설정
group = group.reset_index()
# 피처명을 '판매량'에서 '월간 판매량'으로 변경
group = group.rename(columns={'판매량': '월간 판매량'})
group
각 상점의 상품별 '월간 판매량'을 구했기 때문에, '판매량' 피처 역시 '월간 판매량'으로 바꿨습니다.
이제 train과 group을 병합해 보겠습니다. train은 월ID, 상점ID, 상품ID 조합이므로, 여기에 group을 병합하면 월ID, 상점ID, 상품ID, 월간 판매량 조합을 구할 수 있습니다.
# train과 group 병합하기
train = train.merge(group, on=idx_features, how='left')
train
train 데이터에 각 상점의 상품별 월간 판매량을 추가했습니다. 우리가 원하는 타깃값을 잘 만들었습니다. 그런데 월ID, 상점ID, 상품ID 조합을 만들었기에 타깃값에 결측값이 많습니다. 기존에 없던 조합에는 판매량 정보가 없는 것이 당연합니다. 판매량이 없다는 건 판매량이 0이라는 뜻이니 결측값은 추후 0으로 대체해주도록 하겠습니다.
또한 train을 만드는 일련의 과정에서 sales_train에 있던 date(날짜) 피처가 사라졌습니다. 필요 없는 date 피처를 직접적으로 제거하지 않았지만, 병합되는 과정에서 제외되어 제거한 것과 같은 효과를 얻었습니다.
가비지 컬렉션 group 데이터는 더 이상 필요 없으니 메모리 절약 차원에서 가비지 컬렉션을 해주겠습니다. 가비지 컬렉션(garbage collection)이란 쓰래기 수거라는 뜻으로, 할당한 메모리 중 더는 사용하지 않는 영역을 해제하는 기능입니다. 메모리 관리 기법에 해당합니다. 캐글 노트북 환경이 제공하는 메모리는 한정적입니다. 한정된 메모리를 효율적으로 사용하려면 틈틈이 가비지 컬렉션을 해주는 것이 좋습니다. 데이터 크기가 작을 땐 문제 없지만, 다양한 피처를 만들어서 데이터가 커지면 허용된 메모리를 초과하는 경우가 생깁니다. 그러면 코드가 실행되지 않고 멈춰버리는 문제가 발생합니다. 다음은 group 데이터를 수거하는 코드입니다. 『import gc # 가비지 컬렉터 불러오기 del group # 더는 사용하지 않는 변수 지정 gc.collect(); # 가비지 컬렉션 수행 』 이렇게 간단하게 할 수 있습니다. 가비지 컬렉션을 앞으로도 자주 사용될 예정입니다. |
5. 피처 엔지니어링 Ⅴ : 테스트 데이터 이어붙이기
지금까지 월ID, 상점ID, 상품ID 조합으로 train을 만들고, 여기에 각 상점의 상품별 월간 판매량(타깃값)을 추가했습니다. 이제 train에 테스트 데이터(test)를 이어붙이겠습니다. 테스트 데이터를 이어붙이는 이유는 뒤이어 shops, items, items_categories 데이터를 병합할 예정인데, 이때 테스트 데이터에도 한 번에 병합하는게 좋기 때문입니다.
이어붙이기 전에 test에 월ID 피처를 추가해야 합니다. 월ID 0은 2013년 1월이고, 33은 2015년 10월입니다. 그렇다면 테스트 데이터는 2015년 11월에 해당하므로, 테스트 데이터의 월ID는 34에 해당합니다.
test['월ID'] = 34
test는 ID라는 피처도 가지고 있는데 이는 불필요한 피처입니다. 단지 식별자일 뿐이며, 식별자는 인덱스로 충분합니다. 따라 train에는 'ID 피처를 제거한' test를 이어붙이겠습니다. 이어붙이는 경우에는 판다스 concat()함수를 사용합니다.
# train과 test 이어붙이기
all_data = pd.concat([train, test.drop('ID', axis=1)],
ignore_index=True, # 기존 인덱스 무시(0부터 새로 시작)
keys=idx_features) # 이어붙이는 기준이 되는 피처
앞서 train과 group을 병합하니 결측값이 많았고, train에 test를 이어 붙인다면 test의 월간 판매량에도 결측값이 생깁니다. 월간 판매량은 타깃값인데, 테스트 데이터에는 타깂값이 없기 때문입니다. 따라서 위에서 말한대로 결측값은 모두 0으로 대체하도록 하겠습니다.
# 결측값을 0으로 대체
all_data = all_data.fillna(0)
all_data
위와 보이는 것처럼 테스트 데이터를 이어 붙이고, 모든 결측값을 0으로 바꿨습니다.
6. 피처 엔지니어링 Ⅵ : 나머지 데이터 병합(최종 데이터 생성)
이번에는 추가 정보로 제공된 shops, items, item_categories 데이터를 all_data에 병합하겠습니다. 병합할 때에는 merge()함수를 사용합니다. 추가로, 메모리를 절약하기 위해 데이터 다운캐스팅과 가비지 컬렉션까지 수행하겠습니다.
데이터를 모두 병합했으니 all_data.head()를 출력해서 확인하겠습니다.
모든 데이터가 잘 병합된 것을 알 수 있습니다.
all_data에서 상점명, 상품명, 상품분류명 피처는 모두 러시아어입니다. 문자 데이터이기도 하지만 상점ID, 상품ID, 상품분류ID와 일대일로 매칭되기에 제거해도 되는 피처입니다. 따라 상점명, 상품명, 상품분류명 피처를 제거하겠습니다.
all_data = all_data.drop(['상점명', '상품명', '상품분류명'], axis=1)
이로써 최종 데이터를 만들었습니다.
7. 피처 엔지니어링 Ⅶ : 마무리
앞서 모든 데이터를 병합해 all_data를 만들었습니다. 이제 all_data를 활용해 훈련, 검증, 테스트용 데이터를 만들어보겠습니다. 다음처럼 월ID를 기준으로 나누면 됩니다.
● 훈련데이터 : 2013년 1월부터 2015년 9월(월ID=32)까지 판매 내역
● 검증데이터 : 2015년 10월(월ID=33)판매 내역
● 테스트 데이터 : 2015년 11월(월ID=34)판매 내역
# 훈련 데이터 (피처)
X_train = all_data[all_data['월ID'] < 33]
X_train = X_train.drop(['월간 판매량'], axis=1)
# 검증 데이터 (피처)
X_valid = all_data[all_data['월ID'] == 33]
X_valid = X_valid.drop(['월간 판매량'], axis=1)
# 테스트 데이터 (피처)
X_test = all_data[all_data['월ID'] == 34]
X_test = X_test.drop(['월간 판매량'], axis=1)
# 훈련 데이터 (타깃값)
y_train = all_data[all_data['월ID'] < 33]['월간 판매량']
y_train = y_train.clip(0, 20) # ① 타깃값을 0 ~ 20로 제한
# 검증 데이터 (타깃값)
y_valid = all_data[all_data['월ID'] == 33]['월간 판매량']
y_valid = y_valid.clip(0, 20) # ②
①, ② 에 추가로 넘파이 함수인 clip()을 활용하여 타깃값인 '각 상점의 상품별 월간 판매량'은 0~20사이로 제한했습니다.
clip() 함수의 경우 첫 번째 인수가 하한값이고, 두 번째 인수가 상한값에 해당합니다. 이처럼 값을 하한값과 상한값에서 잘리주는 기법을 클리핑(clipping)이라고 합니다. 이러한 방법은 이상치를 제거할 때도 사용할 수 있습니다.
훈련, 검증, 테스트 데이터를 할당했으니 all_data는 이제 필요 없습니다. 따라 가비지 컬렉션을 해주겠습니다.
del all_data
gc.collect();
이상으로 모델링에 필요한 데이터를 완성했습니다.
8. 모델 훈련 및 성능 검증
베이스라인 모델로는 LightGBM을 사용하겠습니다. 기본 파라미터만 설정하고 LightGBM용 데이터 셋을 만들어 훈련하겠습니다.
train()메서드의 categorical_feature 파라미터만 제외하고는 이전에 했던 '안전 운전자 예측' 코드와 유사합니다. categorical_feature 파라미터에는 범주형 데이터를 전달하면 됩니다. 범주형 데이터로는 상점ID, 상품ID, 상품분류ID가 있습니다. 이중 상품ID를 뺀 상점ID와 상점분류ID만 인수로 전달할 것입니다. 이유는 아래와 같습니다.
상점ID는 고윳값 개수가 상당히 많습니다. LightGBM 문서에 따르면 고윳값 개수가 너무 많은 범주형 데이터는 수치형 데이터로 취급해야 성능이 더 잘 나온다고 합니다. 범주형 데이터는 고윳값 하나하나가 일정한 의미를 갖습니다. 하지만 그 고윳값이 너무 많아져버리면 고윳값이 갖는 의미가 사라지게 됩니다. 그렇기에 수치형 데이터와 별반 다를게 없어지게 됩니다. 이런 이유로 상품ID 피처는 범주형 데이터로 취급하지 않겠습니다.
import lightgbm as lgb
# LightGBM 하이퍼파라미터
params = {'metric': 'rmse', # 평가지표 = rmse
'num_leaves': 255,
'learning_rate': 0.01,
'force_col_wise': True,
'random_state': 10}
# 범주형 피처 설정
cat_features = ['상점ID', '상품분류ID']
# LightGBM 훈련 및 검증 데이터셋
dtrain = lgb.Dataset(X_train, y_train)
dvalid = lgb.Dataset(X_valid, y_valid)
# LightGBM 모델 훈련
lgb_model = lgb.train(params=params,
train_set=dtrain,
num_boost_round=500,
valid_sets=(dtrain, dvalid),
categorical_feature=cat_features,
verbose_eval=50)
이상으로 모델 훈련이 끝났습니다. 검증 데이터로 측정한 RMSE는 1.00722 입니다.
참고로, categorical_feature 파라미터에 아무 값도 전달하지 않으면 category 타입인 데이터를 범주형 데이터로 인식합니다. 다음과 같이 미리 category 타입으로 바꾸면 categorical_feature 파라미터에 범주형 데이터를 전달하지 않아도 모델 훈련 결과가 동일하게 나오게 됩니다.
9. 모델 훈련 및 성능 검증
이제 테스트 데이터를 활용해 타깃값을 예측해보겠습니다. 타깃값은 0~20 사이의 값이어야 하므로 예측한 값 역시 clip()함수를 사용해서 범위를 제한하도록 하겠습니다.
# 예측
preds = lgb_model.predict(X_test).clip(0, 20)
# 제출 파일 생성
submission['item_cnt_month'] = preds
submission.to_csv('submission.csv', index=False)
제출까지 다 만들었습니다.
끝으로 가비지 컬렉션을 해줍니다. 메모리 사용량이 많으면 전체 코드를 재실행할 때 멈출 수 있습니다. 이를 방지려면 [Run] → [Factory reset] 메뉴로 공장 초기화를 하거나 가비지 컬렉션을 해줍니다. 다음은 지금까지 만든 변수를 가비지 컬렉션하는 코드 입니다.
del X_train, y_train, X_valid, y_valid, X_test, lgb_model, dtrain, dvalid
gc.collect();
커밋 후 제출해 보겠습니다.
다음과 같이 1.08534 라는 점수가 나왔습니다.
다음에는 베이스라인 모델이 성능을 개선해보도록 하겠습니다.