과거 판매 데이터를 기반으로 향후 판매량을 예측하는 경진대회를 공략합니다. 탐색적 데이터 분석은 간단하게만 다룹니다. 대신 많은 시간을 피처 엔지니어링에 할애해서 성능 향상을 위한 파생 피처를 만들어봅니다. 이 과정에서 다양한 피처 엔지니어링 기법을 배울 수 있습니다.
학습 순서
향후 판매량 예측은 총 3편입니다. 1편에서는 경진대회 이해와 탐색적 데이터 분석을 알아보고, 2편에서는 베이스라인 모델(LightGBM), 3편에서는 성능 개선(LightGBM + 피처 엔지니어링 강화)를 정리합니다.
학습 키워드
- 유형 및 평가지표 : 회귀, RMSE
- 탐색적 데이터 분석 : 데이터 병합, 그룹화
- 머신러닝 모델 : LightGBM
- 피처 엔지니어링 : 피처명 한글화, 데이터 다운캐스팅, 조합, 이어 붙이기, 병합, 이상치 제거, 파생 피처 생성
파이썬 버전
3.7.10
사용 라이브러리 및 버전
- numpy (numpy==1.19.5)
- pandas (pandas==1.3.2)
- seaborn (seaborn==0.11.2)
- matplotlib (matplotlib==3.4.3)
- sklearn (scikit-learn==0.23.2)
- lightgbm (lightgbm==3.2.1)
- itertools, gc
바로가기 ▶️
1. 베이스라인 모델
데이터를 적절히 처리하여 베이스라인 모델을 만들겠습니다. 베이스라인 모델로는 LightGBM을 사용합니다.
이번 장의 핵심은 피처 엔지니어링입니다. 베이스라인이라서 간단하게만 할 계획이지만, 단계가 많으니 그림으로 먼저 보고 시작하겠습니다.
▼ 베이스라인 모델의 피처 엔지니어링
데이터부터 불러옵니다.
LightGBM으로 범주형 데이터를 모델링하면, 모델링엔 지장이 없지만 불필요한 경고 문구가 뜹니다. ❶은 이 경고 문구를 생략해줍니다.
1.1 피처 엔지니어링 I : 피처명 한글화
본 경진대회는 훈련 데이터를 여러 파일로 제공하고 피처도 다양합니다. 그러다 보니 피처명이 영어면 조금 헷갈릴 수 있습니다. 특히 ‘성능 개선’ 절에서 다양한 파생 피처를 만들어볼 계획인데, 이때 피처를 쉽게 알아보려면 피처명이 한글인 게 좋습니다. sales_train, shops, items, item_categories, test 데이터의 피처명을 모두 한글로 바꿔봅시다. 가장 먼저 sales_train 피처명을 한글로 바꾸겠습니다.
date, date_block_num, shop_id, item_id, item_price, item_cnt_day 피처가 각각 날짜, 월ID, 상점ID, 상품ID, 판매가, 판매량으로 잘 바뀌었죠?
두 번째로 shops 피처명을 바꿔봅시다.
이어서 items 피처명을 바꿉니다.
item_categories 차례입니다.
마지막으로 test 피처명도 한글로 바꾸겠습니다.
이상으로 모든 데이터의 피처명을 한글로 바꿨습니다. 피처를 한눈에 파악하기가 한결 수월해졌죠?
1.2 피처 엔지니어링 II : 데이터 다운캐스팅
다운캐스팅downcasting이란 더 작은 데이터 타입으로 변환하는 작업을 말합니다. 이번 절에서는 데이터 다운캐스팅을 왜 하는지와 그 방법을 배워보겠습니다.
예를 들어 설명해보죠. 금붕어는 금붕어용 어항에서 키우는 게 좋습니다. 돌고래용 수족관을 마련할 필요는 없습니다. 낭비죠. 마찬가지로 데이터가 작은데 큰 데이터 타입을 사용하면 메모리를 낭비하게 됩니다. 주어진 데이터 크기에 딱 맞는 타입을 사용하는 게 좋습니다.
판다스로 데이터를 불러오면 기본적으로 정수형은 int64, 실수형은 float64 타입으로 할당합니다. 각각 정수형, 실수형에서 가장 큰 타입입니다. 모든 피처가 최대 타입을 사용할 필요는 없겠죠. 크지 않은 숫자가 저장된 피처라면 int8, int16, int32, float16, float32 등 보다 작은 타입으로 할당할 수 있습니다. 그래야 메모리 낭비를 막고, 훈련 속도도 빨라집니다.
데이터 다운캐스팅 방법은 부록 A.2에 자세히 설명해놨습니다. 여기서는 부록에서 다룬 함수 중 다음의 downcast( )를 바로 사용하겠습니다. 해당 피처 크기에 맞게 적절한 타입으로 바꿔주는 함수입니다.
이 함수를 이용해 shops, item_categories, items, sales_train, test에 데이터 다운캐스팅하겠습니다.
다운캐스팅 결과 메모리 사용량이 크게 줄어든 것을 볼 수 있습니다.
1.3 피처 엔지니어링 III : 데이터 조합 생성
테스트 데이터의 피처는 ID 피처를 제외하면 상점ID, 상품ID 피처입니다. 우리가 예측해야 하는 값은 각 상점의 상품별 월간 판매량이었죠. 그렇기 때문에 월, 상점, 상품별 조합이 필요합니다. 월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인 상품을 팔지 못했기 때문에 원본 데이터에는 월ID=1, 상점ID=0, 상품ID=5인 데이터가 아예 없습니다. 데이터가 없는 것보다는 ❺ 판매량이 0이더라도 데이터가 있는 게 낫습니다. 의미 있는 데이터는 많을수록 좋기 때문입니다. 앞 그림에서 배경색이 칠해진 부분이 조합하여 새로 만든 데이터입니다. 원본 데이터에 없는 데이터라서 판매량은 모두 0으로 지정했습니다.
지금까지 설명한 데이터 조합을 생성해보겠습니다. 데이터 조합은 itertools가 제공하는 product( ) 함수로 쉽게 만들어낼 수 있습니다.
❶ 이 월ID, 상점ID, 상품ID 피처 조합을 만드는 코드입니다. 월ID의 고윳값(0~33)별로 모든 상점ID 고윳값, 상품ID 고윳값을 구해 조합을 생성합니다. 코드 ❶ 실행 후에 train은 34개 배열(array)을 원소로 갖게 됩니다.
❷ 는 train 내 34개 배열을 하나로 합쳐 DataFrame을 만듭니다.
조합이 잘 생성됐습니다. 9.2.1절에서 살펴본 것처럼 sales_train의 데이터 개수는 2,935,849개입니다. 조합 생성 후 10,913,850개로 3.7배 정도 늘었네요.
참고로, 이렇게 만든 train을 앞으로 훈련 데이터의 뼈대로 사용합니다. 뼈대가 되는 train에 타깃값을 병합하고, 나머지 shops, items, item_categories도 병합할 것입니다.
1.4 피처 엔지니어링 IV : 타깃값( 월간 판매량) 추가
이제부터 train에 다른 데이터도 추가할 건데, 처음은 타깃값인 각 상점의 상품별 월간 판매량입니다.
현재 sales_train에는 일별 판매량을 나타내는 ‘판매량’ 피처가 있습니다. 그런데 우리가 원하는 타깃값은 각 상점의 상품별 ‘월간’ 판매량입니다. 이 값을 구하려면 월ID, 상점ID, 상품ID를 기준으로 그룹화해 판매량을 더해야 합니다. groupby( ) 함수를 활용하면 되겠죠. 앞서 월ID, 상점ID, 상품ID 조합을 만들 때, [‘월ID’, ‘상점ID’, ‘상품ID’]를 idx_features 변수에 할당했습니다.
이 변수를 기준으로 그룹화해서 각 상점의 상품별 월간 판매량을 구해보겠습니다.
각 상점의 상품별 ‘월간 판매량’을 구했으니 피처명 ‘판매량’도 ‘월간 판매량’으로 바꿨습니다.
이제 train과 group을 병합해보죠. train은 월ID, 상점ID, 상품ID 조합이므로, 여기에 group을 병합하면 월ID, 상점ID, 상품ID, 월간 판매량 조합을 구할 수 있습니다.
train 데이터에 각 상점의 상품별 월간 판매량을 추가했습니다. 우리가 원하는 타깃값을 잘 만들었네요. 그런데 타깃값에 결측값이 많습니다. 앞서 월ID, 상점ID, 상품ID 조합을 생성했기 때문입니다. 기존에 없던 조합에는 판매량 정보가 없는 게 당연합니다. 값이 없다는 건 판매량이 0이라는 뜻이니 결측값은 추후 0으로 대체하겠습니다.
또한 train을 만드는 일련의 과정에서 sales_train에는 있던 date(날짜) 피처가 사라졌습니다. 필요 없는 date 피처를 명시적으로 제거한 게 아니라, 병합 과정에서 제외하여 같은 효과를 얻었습니다.
가비지 컬렉션
group 데이터는 더 이상 필요 없으니 메모리 절약 차원에서 가비지 컬렉션을 해주겠습니다. 가비지 컬렉션(garbage collection)이란 쓰레기 수거라는 뜻으로, 할당한 메모리 중 더는 사용하지 않는 영역을 해제하는 기능입니다. 메모리 관리 기법이죠. 캐글 노트북 환경이 제공하는 메모리는 한정적입니다. 한정된 메모리를 효율적으로 사용하려면 틈틈이 가비지 컬렉션을 해주는 게 좋습니다. 데이터 크기가 작을 땐 문제없지만, 다양한 피처를 만들어서 데이터가 커지면 허용된 메모리를 초과하는 경우가 생깁니다. 그러면 코드가 실행되지 않고 멈춰버립니다.
다음은 group 데이터를 수거하는 코드입니다.
간단하죠? 가비지 컬렉션은 앞으로도 자주 사용할 겁니다.
1.5 피처 엔지니어링 V : 테스트 데이터 이어붙이기
지금까지 월ID, 상점ID, 상품ID 조합으로 train을 만들고, 여기에 각 상점의 상품별 월간 판매량(타깃값)을 추가했습니다. 이제 train에 테스트 데이터(test)를 이어붙이겠습니다.* 테스트 데이터를 이어붙이는 이유는 뒤이어 shops, items, item_categories 데이터를 병합할 예정인데, 이때 테스트 데이터에도 한 번에 병합하는 게 좋기 때문입니다.
* 이 책에서는 테이블을 위아래로 합치는 걸 ‘이어붙인다’라고 표현하고, 좌우로 합치는 걸 ‘병합한다’라고 표현합니다. 더불어 이어붙이기에는 concat( ) 함수를, 병합에는 merge( ) 함수를 사용했습니다.
이어붙이기 전에 test에 월ID 피처를 추가해야 합니다. 월ID 0은 2013년 1월이고, 33은 2015년 10월입니다. 테스트 데이터는 2015년 11월 판매 기록입니다. 따라서 테스트 데이터의 월ID는 34로 설정하면 되겠군요.
test는 ID라는 피처도 가지고 있는데 불필요한 피처입니다. 단지 식별자일 뿐이며, 식별자는 인덱스로 충분하기 때문이죠. 따라서 train에는 ‘ID 피처를 제거한’ test를 이어붙이겠습니다. 이어붙이는 데에는 판다스 concat( ) 함수를 사용합니다.
앞서 train과 group을 병합하니 결측값이 많았고, train에 test를 이어붙이면 test의 월간 판매량에도 결측값이 생깁니다. 월간 판매량은 타깃값인데, 테스트 데이터에는 타깃값이 없기 때문입니다. 이어붙인 all_data의 결측값은 0으로 대체하겠습니다.
❶ 테스트 데이터를 이어붙이고, ❷ 결측값을 0으로 바꿨습니다.
1.6 피처 엔지니어링 VI : 나머지 데이터 병합( 최종 데이터 생성)
이번에는 추가 정보로 제공된 shops, items, item_categories 데이터를 all_data에 병합할 겁니다. 병합이므로 merge( ) 함수를 이용합니다. 추가로, 메모리를 절약하기 위해 데이터 다운캐스팅과 가비지 컬렉션까지 수행합니다.
데이터를 모두 병합했으니 all_data.head( )를 출력해볼까요.
모든 데이터가 잘 병합되었습니다.
all_data에서 상점명, 상품명, 상품분류명 피처는 모두 러시아어입니다. 문자 데이터이기도 하거니와 상점ID, 상품ID, 상품분류ID와 일대일로 매칭되므로 제거해도 되는 피처죠. 상점명, 상품명, 상품분류명 피처를 제거하겠습니다.
이로써 최종 데이터를 만들었습니다.
1.7 피처 엔지니어링 VII : 마무리
앞서 모든 데이터를 병합해 all_data를 만들었습니다. 이제 all_data를 활용해 훈련, 검증, 테스트용 데이터를 만들어보겠습니다. 다음처럼 월ID를 기준으로 나누면 됩니다.
- 훈련 데이터 : 2013년 1월부터 2015년 9월(월ID=32)까지 판매 내역
- 검증 데이터 : 2015년 10월(월ID=33) 판매 내역(분석 정리 2)
- 테스트 데이터 : 2015년 11월(월ID=34) 판매 내역
❶ ❷ 추가로 판다스 함수인 clip( )을 활용해 타깃값인 ‘각 상점의 상품별 월간 판매량’은 0~20 사이로 제한했습니다(분석 정리 1). clip( )의 첫 번째 인수가 하한값이고 두 번째 인수가 상한값입니다. 이처럼 값을 하한값과 상한값에서 잘라주는 기법을 클리핑(clipping)이라고 합니다. 이상치를 제거할 때도 사용할 수 있습니다.
훈련, 검증, 테스트 데이터를 할당했으니 all_data는 이제 필요 없습니다. 잊지 말고 가비지 컬렉션을 해줍니다.
이상으로 모델링에 필요한 데이터를 완성했습니다.
1.8 모델 훈련 및 성능 검증
베이스라인 모델로는 LightGBM을 사용하겠습니다. 기본 파라미터만 설정하고 LightGBM용 데이터셋을 만들어서 훈련할 것입니다.
train( ) 메서드의 categorical_feature 파라미터만 제외하고는 8장의 코드와 유사합니다. categorical_feature 파라미터에는 범주형 데이터를 전달하면 됩니다. 범주형 데이터로는 상점ID, 상품ID, 상품분류ID가 있습니다. 이중 상품ID를 뺀 상점ID와 상품분류ID만 인수로 전달할 겁니다. 이유는 다음과 같습니다.
상품ID는 고윳값 개수가 상당히 많습니다. LightGBM 문서에 따르면 고윳값 개수가 너무 많은 범주형 데이터는 수치형 데이터로 취급해야 성능이 더 잘 나온다고 합니다.* 범주형 데이터는 고윳값 하나하나가 일정한 의미를 갖습니다. 그런데 그 고윳값이 너무 많아져버리면 고윳값이 갖는 의미가 상쇄되므로 수치형 데이터와 별반 다를 게 없어지는 거죠. 이런 이유로 상품ID 피처는 범주형 데이터로 취급하지 않겠습니다.
* https://lightgbm.readthedocs.io/en/latest/Advanced-Topics.html#categorical-feature-support
이상으로 모델 훈련이 끝났습니다. 검증 데이터로 측정한 RMSE는 1.00336입니다.
범주형 데이터로 인식하게 하는 다른 방법은?
참고로, categorical_feature 파라미터에 아무 값도 전달하지 않으면 category 타입인 데이터를 범주형 데이터로 인식합니다. 다음과 같이 미리 category 타입으로 바꾸면 categorical_feature 파라미터에 범주형 데이터를 전달하지 않아도 모델 훈련 결과가 같습니다.
cat_features = ['상점ID', '상품분류ID']
for cat_feature in cat_features:
all_data[cat_feature] = all_data[cat_feature].astype('category')
1.9 예측 및 결과 제출
이제 테스트 데이터를 활용해 타깃값을 예측해볼까요. 타깃값은 0~20 사이의 값이어야 하므로 예측한 값도 clip( ) 함수로 범위를 제한하겠습니다.
제출 파일까지 다 만들었습니다.
끝으로 가비지 컬렉션을 해줍니다. 메모리 사용량이 많으면 전체 코드를 재실행할 때 멈출 수 있습니다. 이를 방지하려면 [Run] → [Factory reset] 메뉴로 공장 초기화를 하거나 가비지 컬렉션을 해줍니다. 다음은 지금까지 만든 변수를 가비지 컬렉션하는 코드입니다.
커밋 후 제출해봅시다.
▼ 베이스라인 점수
베이스라인 모델의 퍼블릭 점수는 1.08160입니다. 진행 중인 대회라서 프라이빗 점수는 없습니다. 이 점수면 퍼블릭 리더보드에서 상위 54%입니다. 집필 시점 기준이므로 여러분이 제출할 때는 퍼센트가 달라질 수 있습니다.
3편에서는 베이스라인 모델의 성능을 개선해보겠습니다.
1 Comment