본문 바로가기
ML.DL

[ML 경진대회] 향후 판매량 예측-베이스라인 모델

by 권미정 2022. 11. 27.

<Musthave 머신러닝딥러닝 문제해결 전략> 9장을 실습한 내용입니다.


이제 책의 2부가 끝나가네요. 이번에 참가할 경진대회는 책의 마지막 머신러닝 경진대회인, '향후 판매량 예측' 경진대회입니다. 이번에도 데이터를 처리해 베이스라인 모델을 만들고, 성능 개선을 실습하는 글을 블로깅해 보겠습니다.

 

1. 베이스라인 모델

베이스라인 모델로 LightGBM을 사용합니다. 이번 베이스라인의 단계가 총 7단계나 되는데요! 차근차근 한 단계씩 해 봅시다!

 

먼저 데이터를 불러오겠습니다.

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')

 

1.1 피처 엔지니어링 1: 피처명 한글화

이 경진대회는 훈련 데이터를 여러 파일로 제공하고 피처도 다양한데, 피처를 쉽게 알아보려면 피처명이 한글인 게 좋겠죠? sales_train, shops, items, item_categories, test 데이터의 피처명을 모두 한글로 바꿔 보겠습니다.

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()

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()

모든 피처명이 한글로 잘 바뀐 것을 확인할 수 있습니다. 한눈에 파악하기 쉬워졌네요.

 

1.2 피처 엔지니어링 2: 데이터 다운캐스팅

다운캐스팅이란, 더 작은 데이터 타입으로 변환하는 작업입니다. 데이터가 작은데 큰 데이터 타입을 사용하면 메모리를 낭비하게 되므로, 주어진 데이터 크기에 맞게 작은 타입으로 할당하는 게 좋습니다.

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에 데이터 다운캐스팅하겠습니다.

all_df = [sales_train, shops, items, item_categories, test]
for df in all_df:
    df = downcast(df)

메모리 용량이 크게 줄어들었습니다!

 

1.3 피처 엔지니어링 3: 데이터 조합 생성

테스트 데이터의 피처는 ID 피처를 제외하면 상점ID, 상품ID 피처입니다. 우리가 예측해야 하는 값은 각 상점의 상품별 월간 판매량이기 때문에 월ID, 상점ID, 상품ID 피처 조합이 필요합니다.

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개였는데 조합 생성 후 3.7배 정도 늘어난 것을 볼 수 있습니다.

 

1.4 피처 엔지니어링 4: 타깃값(월간 판매량) 추가

이제 train에 다른 데이터도 추가할 건데, 처음은 타깃값인 각 상점의 상품별 월간 판매량입니다.

현재 sales_train에는 '일별' 판매량을 나타내는 판매량 피처가 있습니다. 그런데 우리는 '월간' 판매량을 구해야 하는데, groupby() 함수를 활용하면 됩니다. 3단계에서 ['월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과 group 병합하기
train = train.merge(group, on=idx_features, how='left')

train

train 데이터에 각 상점의 상품별 월간 판매량을 추가해 원하는 타깃값을 만들었습니다. 그런데 기존에 없던 조합을 생성했기 때문에 판매량 정보가 없어 결측값이 많습니다. 이 결측값은 나중에 0으로 대체하겠습니다.

 

group 데이터는 이제 필요가 없으니 메모리 절약을 위해 가비지 컬렉션을 해 주겠습니다. 가비지 컬렉션이란 할당한 메모리 중 더는 사용하지 않는 영역을 해제하는 기능입니다.

import gc #가비지 컬렉터 불러오기

del group #더는 사용하지 않는 변수 지정
gc.collect(); #가비지 컬렉션 수행

 

1.5 피처 엔지니어링 5: 테스트 데이터 이어붙이기

이제 train에 테스트 데이터를 이어붙여야 하는데, 그전에 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) #이어붙이는 기준이 되는 피처

이어붙인 all_data의 결측값은 0으로 대체하겠습니다.

#결측값을 0으로 대체
all_data = all_data.fillna(0)

all_data

테스트 데이터를 이어붙여서 월ID가 34가 됐고, 결측값은 0으로 바뀌었습니다.

 

1.6 피처 엔지니어링 6: 나머지 데이터 병합(최종 데이터 생성)

이번엔 추가 정보로 제공된 나머지 데이터를 all_data에 병합하기 위해 merge() 함수를 이용하고, 메모리 절약을 위한 데이터 다운캐스팅과 가비지 컬렉션을 수행하겠습니다.

#나머지 데이터 병합
all_data = all_data.merge(shops, on='상점ID', how='left')
all_data = all_data.merge(items, on='상품ID', how='left')
all_data = all_data.merge(item_categories, on='상품분류ID', how='left')

#데이터 다운캐스팅
all_data = downcast(all_data)

#가비지 컬렉션
del shops, items, item_categories
gc.collect();

all_data.head()

모든 데이터가 잘 병합되었네요!

 

all_data에서 상점명, 상품명, 상품분류명 피처는 모두 러시아어입니다. 문자 데이터이기도 하고 상점ID, 상품ID, 상품분류ID와 일대일로 매칭되므로 제거해도 되는 피처입니다. drop() 함수로 제거하겠습니다.

all_data = all_data.drop(['상점명', '상품명', '상품분류명'], axis=1)

최종 데이터가 만들어졌습니다!

 

1.7 피처 엔지니어링 7: 마무리

이제 all_data를 활용해 훈련, 검증, 테스트용 데이터를 만들어 보겠습니다. 월ID를 기준으로 나누면 됩니다.

#훈련 데이터 (피처): 2015년 9월(월ID=32)까지 판매 내역
X_train = all_data[all_data['월ID'] < 33]
X_train = X_train.drop(['월간 판매량'], axis=1)
#검증 데이터 (피처): 2015년 10월(월ID=33) 판매 내역
X_valid = all_data[all_data['월ID'] == 33]
X_valid = X_valid.drop(['월간 판매량'], axis=1)
#테스트 데이터 (피처): 2015년 11월(월ID=34) 판매 내역
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) #타깃값을 0~20로 제한

추가로 넘파이 함수 clip()을 활용해 타깃값인 '각 상점의 상품별 월간 판매량'은 0~20으로 제한했습니다. 이처럼 값을 하한값과 상한값에서 잘라주는 기법을 클리핑이라고 합니다.

 

훈련, 검증, 테스트 데이터를 할당해서 all_data는 이제 필요 없으니 가비지 컬렉션을 해 주겠습니다.

del all_data
gc.collect();

 

1.8 모델 훈련 및 성능 검증

베이스라인 모델로 LightGBM을 사용할 건데, train() 메서드의 categorical_feature 파라미터만 제외하고는 지난 경진대회에서 사용한 코드와 비슷합니다. categorical_feature 파라미터에는 범주형 데이터인 상점ID, 상품ID, 상품분류ID를 전달하면 됩니다. 그런데 상품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입니다.

 

1.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)

마지막으로 가비지 컬렉션을 해 줍니다.

del X_train, y_train, X_valid, y_valid, X_test, lgb_model, dtrain, dvalid
gc.collect();

 

커밋 후 제출해 보겠습니다!

베이스라인 모델의 퍼블릭 점수는 1.08534입니다. 진행 중인 대회라서 프라이빗 점수는 없습니다.

 

 

지금까지 베이스라인 모델을 생성했는데요! 다음 글에서는 성능 개선을 진행해 보겠습니다.

댓글