과거 판매 데이터를 기반으로 향후 판매량을 예측하는 경진대회를 공략합니다. 탐색적 데이터 분석은 간단하게만 다룹니다. 대신 많은 시간을 피처 엔지니어링에 할애해서 성능 향상을 위한 파생 피처를 만들어봅니다. 이 과정에서 다양한 피처 엔지니어링 기법을 배울 수 있습니다.
학습 순서
향후 판매량 예측은 총 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입니다. 따라서 성능을 더 높이려면 하이퍼파라미터를 최적화하거나 피처 엔지니어링을 강화해야 합니다. 하이퍼파라미터 최적화는 6~8장에서 살펴보았으니, 이번 장에서는 피처 엔지니어링에 집중하겠습니다. 다양한 피처를 만들 계획입니다. 본 경진대회에서는 피처 엔지니어링할 요소가 많습니다. 피처 엔지니어링이 이번 절의 거의 전부라고 봐도 무방할 정도입니다.
이번 절에서 다룰 피처 엔지니어링은 다음과 같습니다.
- 베이스라인과 똑같이 피처명 한글화와 데이터 다운캐스팅을 합니다.
- 개별 데이터, 즉 sales_train, shops, items, item_categories를 활용해 전처리, 파생 피처 생성, 인코딩을 수행합니다.
- 베이스라인과 동일하게 데이터 조합을 만들 겁니다. 이어서 몇 가지 파생 피처를 추가합니다.
- 테스트 데이터를 합친 후, 2번에서 피처 엔지니어링한 다른 데이터들을 병합합니다.
- 시차 피처를 만듭니다. 시차 피처를 만들려면 먼저 ‘기준 피처별 월간 평균 판매량 피처’를 구해야 합니다. 이 피처를 기반으로 시차 피처를 만들 것입니다.
- 마지막으로 그 외 추가적인 피처 엔지니어링을 적용합니다.
1, 3, 4번은 베이스라인과 거의 같고, 2, 5, 6번을 추가로 수행하는 것입니다. 이렇게 공들여 만든 데이터로 LightGBM 모델을 훈련하여 성능이 얼마나 좋아지는지 보겠습니다.
가장 먼저 데이터를 불러옵니다. 코드는 베이스라인과 똑같습니다.
1.1 피처 엔지니어링 I : 피처명 한글화와 데이터 다운캐스팅
베이스라인과 똑같이 피처명을 한글화합니다.
데이터 다운캐스팅까지도 베이스라인과 동일하게 진행합니다.
62.5% 압축됨
38.6% 압축됨
54.2% 압축됨
39.9% 압축됨
70.8% 압축됨
1.2 피처 엔지니어링 II : 개별 데이터 피처 엔지니어링
이번 절에서는 sales_train, shops, items, item_categories 데이터를 ‘각각’ 피처 엔지니어링하겠습니다.
sales_train 이상치 제거 및 전처리
첫 번째로 sales_train 데이터의 이상치를 제거하고 간단하게 전처리하겠습니다. 1편 데이터 둘러보기에서 본 바와 같이 sales_train은 날짜, 월ID, 상점ID, 상품ID, 판매가, 판매량 피처를 갖습니다. 여기서 판매가와 판매량 피처의 이상치를 제거할 것입니다. 이상치가 있으면 성능이 나빠질 우려가 있기 때문이죠.
판매가, 판매량이 음수라면 환불 건이거나 오류입니다. 따라서 판매가, 판매량이 음수인 데이터는 이상치로 간주하겠습니다. 또한 9.2.2절에서 설명한 것처럼 판매가가 50,000 이상인 데이터, 판매량이 1,000 이상인 데이터도 이상치로 간주하겠습니다(분석 정리 9). 결론적으로 판매가가 0~50,000 사이이고, 판매량이 0~1,000 사이인 데이터만 추출하겠습니다.
sales_train 데이터에서 이상치를 제거했습니다.
이번에는 간단한 데이터 전처리를 해보겠습니다. 상점명을 조금 다르게 기입해서 같은 상점인데 따로 기록돼 있는 상점이 네 쌍 있습니다. 러시아어를 모르면 정확히 판단하기 어렵지만 참고한 코드에서 다음 상점명 네 쌍을 같은 의미로 간주했습니다. shops 데이터를 잠깐 활용해 상점명이 유사한 네 쌍을 출력해보겠습니다.
!Якутск Орджоникидзе, 56 фран || Якутск Орджоникидзе, 56
!Якутск ТЦ "Центральный" фран || Якутск ТЦ "Центральный"
Жуковский ул. Чкалова 39м? || Жуковский ул. Чкалова 39м²
РостовНаДону ТРК "Мегацентр Горизонт" || РостовНаДону ТРК "Мегацентр Горизонт" Островной
상점ID는 0부터 차례로 번호가 매겨져 있습니다. 그렇기 때문에 0번째 상점명 데이터는 상점ID가 0인 데이터와 같다는 점을 참고해주세요.
비교하기 편하게 각 쌍을 위아래로 나란히 배치해보죠. 차이가 나는 부분에 음영을 칠했습니다.
- 0vs.57
- !Якутск Орджоникидзе, 56 фран
- Якутск Орджоникидзе, 56
- 1vs.58
- !Якутск ТЦ “Центральный” фран
- Якутск ТЦ “Центральный”
- 10 vs. 11
- Жуковский ул. Чкалова 39м?
- Жуковский ул. Чкалова 39м2
- 39 vs. 40
- РостовНаДону ТРК “Мегацентр Горизонт”
- РостовНаДону ТРК “Мегацентр Горизонт” Островной
러시아어를 모르지만 이렇게 놓고 보니 확실히 거의 같아 보입니다. 사전을 찾아보니 처음 두 줄에서 에서 누락된 단어 фран는 우리말로 ‘거리’라는 뜻입니다.
따라서 sales_train과 test 데이터에서 상품ID 0은 57로, 1은 58로, 10은 11로, 39는 40으로 수정하겠습니다. 상점명은 놔두고 상품ID를 수정합니다. 이유는 상점명과 상점ID가 결국 일대일 매칭되고, 상점명은 문자 데이터라 나중에 제거할 예정이기 때문입니다.
shops 파생 피처 생성 및 인코딩
이번에는 shops 데이터에서 새로운 피처를 만들고 인코딩까지 해보겠습니다.
9.3.1절에서 확인했듯이 shops에도 상점명이 러시아어로 기록돼 있습니다. 고맙게도 다른 캐글러가 상점명의 첫 단어가 도시라는 사실을 알아냈습니다. 즉, 상점명 첫 단어는 상점이 위치한 도시를 뜻합니다. 상점명을 활용해 도시 피처를 만들 수 있겠군요. 상점명 피처를 공백 기준으로 나눈 뒤, 첫 번째 단어를 가져오면 됩니다.
도시 피처가 잘 만들어졌는지 확인해보겠습니다.
array(['!Якутск', 'Адыгея', 'Балашиха', 'Волжский', 'Вологда', 'Воронеж',
'Выездная', 'Жуковский', 'Интернет-магазин', 'Казань', 'Калуга',
'Коломна', 'Красноярск', 'Курск', 'Москва', 'Мытищи', 'Н.Новгород',
'Новосибирск', 'Омск', 'РостовНаДону', 'СПб', 'Самара', 'Сергиев',
'Сургут', 'Томск', 'Тюмень', 'Уфа', 'Химки', 'Цифровой', 'Чехов',
'Якутск', 'Ярославль'], dtype=object)
그대로 복사해 구글에 검색해보면 러시아 도시명이라는 사실을 알 수 있습니다. 그런데 맨 처음 도시명 앞에 느낌표(!)가 있네요. 특수 문자가 잘못 기재된 것이니 제거하겠습니다. 참고로 Якутск는 ‘야쿠츠크’라는 도시입니다.
도시명은 범주형 피처입니다. 머신러닝 모델은 문자를 인식하지 못하므로 숫자로 바꿔야 합니다. 인코딩을 해야겠죠? 여기서는 가장 간단한 레이블 인코딩을 적용하겠습니다.
상점명 피처를 활용해 도시 피처를 만들어 인코딩까지 마쳤습니다. 이제 상점명 피처는 모델링에 더는 필요가 없습니다. 같은 의미가 상점ID 피처에 내포돼 있기 때문입니다. 그러니 상점명 피처는 제거합니다.
▼ 실행 결과
최종적으로 shops에는 상점ID와 도시 피처가 남게 되며, 각 상점이 어느 도시에 위치하는지를 나타냅니다.
items 파생 피처 생성
이번에는 items를 활용해 ‘첫 판매월’ 피처를 구해보겠습니다. items 데이터는 상품명, 상품ID, 상품분류ID를 피처로 갖습니다. 우선, 상품명 역시 상품ID와 일대일 매칭되어 있어서 제거해도 됩니다.
다음으로 상품이 맨 처음 팔린 월을 피처로 만들어보겠습니다. items 데이터만으로 만들 수는 없어서 sales_train 데이터를 이용하겠습니다. sales_train을 상품ID 기준으로 그룹화한 뒤, 그 그룹에서 월ID 최솟값을 구하면 됩니다. 이게 무슨 말일까요? sales_train 데이터는 판매 내역 데이터입니다. 판매 내역 데이터에서 상품ID가 가장 처음 등장한 날의 월ID를 구하는 겁니다. 해당 상품이 처음 팔린 달을 구하는 거죠. groupby( )와 집계 함수 ‘min’을 사용해서 첫 판매월 피처를 구해보겠습니다.
▼ 실행 결과
첫 판매월 피처가 잘 추가됐네요. 그런데 이 피처에 결측값이 있습니다.
▼ 실행 결과
결측값이 368개나 있군요. 여기서 결측값은 무엇을 의미할까요? 해당 상품이 한 번도 판매된 적이 없다는 뜻입니다. 이 결측값은 어떻게 처리해야 할까요?
훈련 데이터는 2013년 1월부터 2015년 10월까지의 판매 내역입니다. 월ID는 0부터 33입니다. 테스트 데이터는 2015년 11월 판매 내역입니다. 월ID는 34죠. 2013년 1월부터 2015년 10월까지 한 번도 팔리지 않은 상품이 있다면 그 상품이 처음 팔린 달을 2015년 11월이라고 가정해도 됩니다. 첫 판매월 피처의 결측값을 34로 대체하면 된다는 말입니다.
물론 2015년 11월에도 안 팔릴 수 있습니다. 하지만 그건 문제되지 않습니다. 어차피 테스트 데이터에 없는 상품이면 아예 고려 대상이 아니기 때문이죠(테스트 데이터에도 없다는 말은 2015년 11월에도 안 팔렸다는 뜻입니다). 고려 대상이 아닌 데이터의 첫 판매월이 34든 다른 값이든 문제되지 않습니다.
▼ 첫 판매월 결측값을 34로 대체(예시)
따라서 첫 판매월 피처의 결측값을 34로 대체하겠습니다.
item_categories 파생 피처 생성 및 인코딩
이번엔 item_categories에서 ‘대분류’라는 파생 피처를 만들고, 이를 인코딩해보겠습니다. item_categories는 상품분류명을 담고 있습니다. 역시 러시아어인데, 상품분류명의 첫 단어가 범주 대분류라는 점을 이번에도 다른 캐글러가 발견했습니다. 이를 참고해서 대분류를 추출하겠습니다.
예컨대 고양이, 호랑이, 사자는 고양이과 동물이고 개, 늑대, 여우는 개과 동물입니다. 고양이과 동물과 개과 동물을 합쳐서 포유류라는 더 큰 범주로 간주할 수 있습니다. 이처럼 상품분류를 더 큰 범주로 묶는 작업을 하는 것입니다. 범주가 많을 때는 몇 가지로 묶어 더 큰 범주로 만드는 걸 고려해볼 수 있습니다. 전체 데이터 개수는 고정되어 있으므로 범주형 데이터가 지나치게 세밀하면, 자연스럽게 각 범주에 해당하는 데이터의 절대 개수가 적어져서 제대로 훈련되지 못할 수 있습니다. 이럴 때 큰 범주로 묶으면 범주별 훈련 데이터 수가 많아져서 대분류 수준에서는 예측 정확도가 높아지는 거죠. 따라서 큰 범주로 묶으면 범주가 지나치게 세밀할 때보다 성능 향상에 유리할 수 있습니다.
상점명에서 도시명을 추출한 코드와 같은 방식으로 상품분류명에서 대분류 피처를 추출해보겠습니다.
새로 만든 대분류 피처의 고윳값 개수를 출력해봅시다.
Игры 14
Книги 13
Подарки 12
Игровые 8
Аксессуары 7
Музыка 6
Программы 6
Карты 5
Кино 5
Служебные 2
Чистые 2
PC 1
Билеты 1
Доставка 1
Элементы 1
Name: 대분류, dtype: int64
참고로 고윳값 개수가 가장 많은 Игры은 ‘게임’, Книги은 ‘책’, Подарки은 ‘선물’을 뜻합니다.
여기서 고윳값이 5개 미만인 대분류는 모두 ‘etc’로 바꾸겠습니다. 대분류 하나가 범주를 일정 개수 이상을 갖는 게 성능 향상에 유리하기 때문입니다.
처리가 잘 됐는지 item_categories.head( )를 출력해봅니다.
▼ 대분류 피처 생성 후 item_categories.head() 출력
이어서 범주형 피처인 대분류를 인코딩하고, 더 이상 필요 없는 상품분류명 피처는 제거하겠습니다.
이번 절에서 각 데이터에 적용한 피처 엔지니어링은 다음과 같습니다.
▼ 각 데이터에 적용한 피처 엔지니어링
1.3 피처 엔지니어링 III : 데이터 조합 및 파생 피처 생성
이번에도 베이스라인과 마찬가지로 먼저 데이터 조합을 생성합니다. 그런 다음 월간 판매량, 평균 판매가, 판매건수 피처를 만듭니다.
데이터 조합
베이스라인과 같은 방식으로 월ID, 상점ID, 상품ID 조합을 생성합니다.
파생 피처 생성
이번에는 총 3개의 피처를 새로 만들겠습니다.
먼저 월ID, 상점ID, 상품ID별 ‘월간 판매량’과 ‘평균 판매가’ 피처를 만듭니다. 베이스라인에서는 타깃값인 월간 판매량만 만들었지만, 여기서는 평균 판매가도 만듭니다. 다양한 파생 피처를 만들기 위해서입니다.
▼ 실행 결과
타깃값인 월간 판매량과 새로운 파생 피처인 평균 판매가를 추가했습니다. 그런데 결측값이 있네요. 결측값이 있다는 건 판매량과 판매가가 0이라는 뜻입니다. 나중에 결측값은 0으로 대체하겠습니다. 여기서 group은 임시로 만든 변수이므로 가비지 컬렉션을 해줍니다.
세 번째로 만들 파생 피처는 ‘기준 피처별 상품 판매건수’입니다. 상품 월간 판매량과는 다른 개념입니다. 상품 판매량이 엊그제 3개, 어제 0개, 오늘 2개라면 월간 판매량은 5개지만, 판매건수는 2건입니다. 당일에 한 번이라도 판매했다면 건수가 1건이고, 판매를 못 했다면 0건입니다. 집계 함수로 count를 써서 기준 피처별 상품 판매건수를 구할 수 있습니다.
▼ 실행 결과
이상으로 train에 월간 판매량, 평균 판매가, 판매건수를 추가했습니다. 판매건수의 결측값도 뒤에서 0으로 대체하겠습니다.
1.4 피처 엔지니어링 IV : 데이터 합치기
테스트 데이터를 이어붙이고, 9.4.2절에서 피처 엔지니어링한 sales_train, shops, items, item_categories 데이터를 병합하겠습니다.
테스트 데이터 이어붙이기
이제 train에 테스트 데이터를 이어붙입니다. 코드는 베이스라인과 똑같습니다.
▼ 실행 결과
train에 test를 이어붙여 all_data를 만들었습니다.
모든 데이터 병합
all_data에 shops, items, item_categories 데이터를 병합합니다. 이어서 데이터 다운캐스팅까지 같이 해주겠습니다. 이번 코드도 베이스라인과 같습니다.
64.6% 압축됨
shops, items, item_categories는 all_data에 병합됐으니 더 이상 필요 없습니다. 그러니 가비지 컬렉션을 해줍니다.
1.5 피처 엔지니어링 V : 시차 피처 생성
이번에는 시차 피처를 만들어볼 계획입니다. 시차 피처time lag feature란 과거 시점에 관한 피처로, 성능 향상에 도움되는 경우가 많아서 시계열 문제에서 자주 만드는 파생 피처입니다. 그러니 시차 피처 만드는 법을 잘 알아두면 좋겠죠?
💡 Note: 시차 피처는 시계열 문제의 성능 향상에 도움되는 경우가 많습니다.
시차 피처를 만들려면 기준으로 삼을 피처를 먼저 정해야 합니다. 이번 경진대회라면 타깃값과 관련된 ‘월간 평균 판매량’이 좋겠네요. 그렇기 때문에 시차 피처를 구하기에 앞서 ‘기준 피처별 월간 평균 판매량’ 피처를 만들어야 합니다. 여러 기준 피처별 월간 평균 판매량 피처를 구한 뒤, 이 피처를 징검다리 삼아 시차 피처를 만들 계획입니다.
▼ 시차 피처 생성 절차
그럼 먼저 ‘월간 평균 판매량’ 피처를 만들어볼까요?
기준 피처별 월간 평균 판매량 파생 피처 생성
월간 평균 판매량을 구할 때 기준 피처는 다양하게 정할 수 있습니다. 예를 들어 상점별 월간 평균 판매량, 상품별 월간 평균 판매량, 각 상점의 상품별 월간 평균 판매량 등 다양하죠.
기준 피처로 그룹화해 월간 평균 판매량을 구해주는 함수를 만들어보겠습니다. 파라미터는 총 3개입니다.
- df : 작업할 전체 데이터(DataFrame)
- mean_features : 새로 만든 월간 평균 판매량 파생 피처명을 저장하는 리스트
- idx_features : 기준 피처
여기서 기준 피처의 첫 번째 요소는 반드시 ‘월ID’여야 합니다. ‘월간’ 평균 판매량 파생 피처를 만들 것이기 때문이죠. 그리고 기준 피처 개수는 2개나 3개로 설정했습니다. 기준 피처 개수가 많으면 과도하게 세분화되기 때문입니다.
❶ 기준 피처의 첫 번째 요소가 ‘월ID’가 맞는지, 기준 피처 개수가 2개 혹은 3개인지 확인합니다. 아닐 경우 오류를 발생시킵니다.
❷ 파생 피처명을 설정합니다. 기준 피처 개수가 2개일 때와 3개일 때로 나눠 설정했습니다.
❸ 기준 피처를 토대로 그룹화해 월간 평균 판매량을 구합니다. 이렇게 구한 파생 피처의 이름은 ❷에서 정의한 feature_name으로 설정합니다.
❹ ‘원본데이터인df’와 ❸에서 구한 group’을 병합합니다. 병합할 때 기준이 되는 피처는 idx_features이며, 병합 조건은 ‘left’입니다. 원본 데이터를 기준으로 병합해야 해서 병합 조건이 ‘left’인 겁니다. 이로써 파생 피처가 원본 데이터에 추가됩니다.
❺ 이어서 데이터를 다운캐스팅합니다. downcast( ) 함수의 두 번째 파라미터 verbose에 False를 전달하면 몇 퍼센트 압축됐다는 문구를 출력하지 않습니다.
❻ 새로 만든 파생 피처명을 mean_features 리스트에 추가합니다. 추후 시차 피처를 만드는 데 사용하기 위해서입니다.
❼ 마지막으로 가비지 컬렉션을 합니다. ❽ 최종 반환값은 파생 피처가 추가된 df와 파생 피처명 이 추가된 mean_features입니다.
지금까지 add_mean_features( ) 함수의 기능과 구현을 알아봤습니다. 그럼 이 함수를 이용해 기준 피처별 월간 평균 판매량 파생 피처를 만들어볼까요?
먼저 [‘월ID’, ‘상품ID’]로 그룹화한 월간 평균 판매량과 [‘월ID’, ‘상품ID’, ‘도시’]로 그룹화한 월간 평균 판매량을 만들겠습니다. 기준 피처가 [ ‘월ID’, ‘상품ID’]와 [ ‘월ID’, ‘상품ID’, ‘도시’]라는 말입니다.
💡 Note: 어떤 피처를 기준으로 그룹화해야 예측 성능이 좋아지는지 미리 확실하게 알 수는 없습니다. 다른 기준 피처로 그룹화해서 새로운 파생 피처를 만들 수도 있으니 창의력을 발휘해서 더 나은 파생 피처를 만들어보세요.
item_mean_features는 ‘기준 피처에 상품ID를 포함하는 파생 피처’를 저장하는 리스트입니다. 추후 이 리스트에 저장된 파생 피처를 활용해 추가적인 피처 엔지니어링(시차 피처 생성)을 적용할 예정입니다.
여기서 생성한 파생 피처는 ❶ ‘상품ID별 평균 판매량’과 ❷ ‘상품ID 도시별 평균 판매량’입니다. add_mean_features( )를 실행하면 ‘상품ID별 평균 판매량’과 ‘상품ID 도시별 평균 판매량’ 피처가 각각 all_data에 추가됩니다. item_mean_features에 파생 피처명이 잘 추가됐는지 봅시다.
['상품ID별 평균 판매량', '상품ID 도시별 평균 판매량']
이번에는 [‘월ID’, ‘상점ID’, ‘상품분류ID’]를 기준 피처로 그룹화해 월간 평균 판매량을 구해보겠습니다. 파생 피처명은 ‘상점ID 상품분류ID별 평균 판매량’이 되겠죠?
보다시피 기준 피처 중 ‘상점ID’를 포함한 파생 피처명은 shop_mean_features 리스트에 따로 담았습니다(앞서 ‘상품ID’를 포함한 파생 피처명은 item_mean_features 리스트에 담았습니다). 추후 시차 피처를 만들 때 경우를 나눠 생성할 예정이기 때문입니다.
['상점ID 상품분류ID별 평균 판매량']
새로운 파생 피처 ‘상점ID 상품분류ID별 평균 판매량’을 잘 생성했네요.
시차 피처 생성 원리 및 함수 구현
지금까지 만든 기준 피처별 월간 평균 판매량 피처는 다음과 같습니다.
- {상품ID}별 평균 판매량
- {상품ID + 도시}별 평균 판매량
- {상점ID + 상품범주ID}별 평균 판매량
이제 이 세 징검다리 피처를 활용해 시차 피처를 구해보겠습니다. 앞서 언급했듯이 시차 피처는 시계열 문제에서 자주 사용하는 파생 피처입니다. 현시점 데이터에 과거 시점 데이터를 추가한다는 개념입니다. 과거 시점 데이터는 향후 판매량 예측에 유용하기 때문에 사용합니다. 시차 피처는 한 달 전, 두 달 전, 세 달 전 등 원하는 시점까지 생성할 수 있습니다. 하지만 시점이 너무 과거면 예측력이 오히려 떨어질 수 있으므로 여기서는 세 달 전까지만 만들겠습니다.
시차 피처 생성 절차는 다음과 같습니다. 한 달 전 시차 피처 생성을 예로 설명해보죠. 준비한 그림과 비교해가며 차근차근 읽어보시기 바랍니다.
▼ 시차 피처 생성 원리
- 기준 피처와 ‘시찻값을 구하려는 피처’를 정합니다. 여기서는 기준 피처를 ‘월ID’, ‘상점ID’, ‘상품ID’로 정하고, 시찻값을 구하려는 피처를 ‘월간 판매량’으로 정했습니다.
- 원본 데이터인 df에서 기준 피처와 월간 판매량 피처만 추출해 복사본을 만듭니다. 이 복사본을 df_temp라고 정의합니다.
- 새로 만들 시차 피처명을 정합니다. ‘월간 판매량_시차1’로 정했습니다.
- df_temp에서 ‘월간 판매량’ 피처명을 ‘월간 판매량_시차1’로 바꿉니다.
- df_temp의 ‘월ID’ 피처에 1을 더합니다(한 달 시차를 생성하는 작업).
- 기준 피처를 토대로 df와 df_temp를 병합합니다.
이렇게 하면 df에 한 달 전 시차 피처가 생성됩니다. df에는 월간 판매량뿐만 아니라 한 달 전 월간 판매량까지 있는 거죠.
다음의 add_lag_features( )는 방금 설명한 원리를 적용해 만든 시차 피처를 추가하는 함수입니다. 파라미터는 다음과 같습니다.
- df : 원본 데이터
- lag_features_to_clip : ‘값의 범위를 0~20 사이로 제한할 피처’를 담을 리스트.판매량 관련 피처가 해당(9.1절 참고)
- idx_features : 기준 피처
- lag_feature : 시차를 만들 피처
- nlags : 시차
- 1=한달전시차피처만생성
- 2=한달전,두달전시차피처생성
- 3=한달전,두달전,세달전시차피처를모두생성
- clip : 새로 만든 시차 피처를 lag_features_to_clip 리스트에 저장할지 여부(True 혹은 False). lag_features_to_clip 리스트에 들어 있는 피처 값은 나중에 0~20 사이로 제한됨
❶ ****먼저 원본 데이터인 df에서 원하는 피처만 추출해 복사본을 만듭니다. 기준 피처인 idx_features와 시차 적용 피처인 lag_feature만 추출했습니다. 이때 idx_features는 리스트 타입이고, lag_feature는 문자열 타입입니다. 리스트와 문자열은 바로 합칠 수 없습니다. 그래서 idx_features + [lag_feature]와 같이 합쳤습니다. 이렇게 원하는 피처만 추출한 데이터의 복사본을 만들어(copy( ) 메서드) df_temp에 저장했습니다.
❷ 이어서 nlags 인수의 값만큼 for문을 돌며 시차 피처를 생성합니다.
❸ 새로 만들 시차 피처명을 지정합니다.
❹ df_temp의 열 이름도 설정합니다. lag_feature 피처명을 ❸에서 만든 lag_feature_name으로 바꾼 겁니다. 새로 만들 시차 피처명으로 바꾼 거죠.
❺ 다음으로 df_temp의 월ID 피처에 i를 더합니다. 이는 시차 피처를 만드는 핵심 역할을 합니다. 나머지 피처값은 그대로지만 월(월ID)이 한 달씩 밀린 겁니다(i=1인 경우). 왜 한 달씩 미룰까요? 그래야 앞의 ‘피처 생성 원리’ 그림처럼 한 달 전 시차 피처를 만들 수 있기 때문이죠. df와 df_temp를 idx_features 기준으로 병합하면 한 달 전 시차 피처를 만들 수 있습니다.
❻ df와 df_temp를 병합합니다. df_temp에 중복된 행이 있을 수 있으니 drop_duplicates( ) 함수로 중복된 행은 제거한 뒤 병합합니다.
한편 병합할 때 매달 데이터가 반드시 있는 건 아니므로 한 달 전 피처가 없을 수도 있습니다. 그런 경우에는 병합 후 시차 피처에 결측값이 생깁니다. 이 결측값을 ❼에서 0으로 대체합니다.
❽ clip 인수가 True면 방금 만든 시차 피처명을 lag_features_to_clip 리스트에 추가합니다. 이상으로 시차 피처 생성을 마쳤습니다.
❾ 그런다음데이터다운캐스팅을하고,가비지컬렉션까지마칩니다.
❿ 최종적으로 시차 피처가 추가된 DataFrame과 0~20 사이로 제한할 시차 피처명이 저장된 lag_features_to_clip을 반환합니다.
코드가 조금 복잡하죠? 앞서 설명한 절차와 코드를 다시 보면서 꼭 숙지하시기 바랍니다. 시계열 문제에서 시차 피처를 만드는 데 자주 활용하는 방법이니, 알아두면 다른 시계열 문제를 다룰 때도 응용할 수 있습니다.
이제부터 이 함수를 이용해 몇 가지 시차 피처를 생성하겠습니다.
시차 피처 생성 I : 월간 판매량
기준 피처는 ‘월ID’, ‘상점ID’, ‘상품ID’로 하여 월간 판매량의 세 달치 시차 피처를 만들어보죠. clip=True를 전달해 세 달치 시차 피처를 lag_features_to_clip 리스트에 저장해두겠습니다. 월간 판매량은 타깃값이므로 0~20 사이로 제한해야 하기 때문입니다.
시차 피처가 잘 만들어졌는지 봅시다. 피처가 많아서 행과 열을 바꿔 출력하겠습니다. DataFrame의 행과 열을 바꿔 출력하려면 T 메서드를 사용하면 된다는 점 기억하시죠?
▼ 실행 결과
월간 판매량_시차1, 월간 판매량_시차2, 월간 판매량_시차3 피처가 잘 만들어졌습니다. nlags=3이니 시차 피처를 3개 만든 것입니다.
새로 만든 세 피처의 이름은 lag_features_to_clip에 저장돼 있습니다.
['월간 판매량_시차1', '월간 판매량_시차2', '월간 판매량_시차3']
시차 피처 생성 II : 판매건수, 평균 판매가
이어서 판매건수와 평균 판매가의 시차 피처도 만들겠습니다. 판매건수와 평균 판매가는 타깃값이 아니라서 0~20 사이로 제한할 필요가 없습니다. 그렇기 때문에 clip 파라미터는 생략했습니다(기본값이 False).
시차 피처 생성 III : 평균 판매량
이번에는 다른 시차 피처를 만들어보겠습니다. 앞서 평균 판매량 피처를 만들 때 item_mean_features와 shop_mean_features에 평균 판매량 피처를 저장한 것 기억하시죠? 두 리스트에 저장된 평균 판매량 피처를 활용해서도 시차 피처를 만들 수 있습니다.
item_mean_features에는 ‘상품ID별 평균 판매량’과 ‘상품ID 도시별 평균 판매량’이 저장돼 있습니다. 먼저 두 피처값에 대해서 시차 피처를 생성하겠습니다. item_mean_features를 순회하며 시차 피처를 만들어보죠. 다음 코드는 ‘월ID’, ‘상점ID’, ‘상품ID’ 기준으로 ‘상품ID별 평균 판매량’과 ‘상품ID 도시별 평균 판매량’의 시차 피처를 만듭니다.
시차 피처를 만든 후에는 all_data에서 item_mean_features에 저장된 피처들을 제거했습니다. 이 피처들은 시차 피처를 만드는 데 필요할 뿐 모델링에 사용하진 않기 때문입니다.
다음으로 shop_mean_features를 활용해 시차 피처를 구해보죠. shop_mean_features에는 ‘상점 상품분류ID별 평균 판매량’ 피처가 저장돼 있습니다. 요소가 한 개라서 반복문을 순회할 필요는 없습니다. 그렇지만 다른 파생 피처를 더 만들 경우에 대비해 반복문으로 코드를 짜뒀습니다. 이번에는 기준 피처를 ‘월ID’, ‘상점ID’, ‘상품분류ID’로 했습니다. 기준 피처를 idx_features로 해도 문제 없습니다. 단지 기준 피처를 다르게 잡아본 겁니다(파생 피처 만드는 방식에는 정답이 없다고 했죠?).
이번에도 시차 피처를 생성한 뒤 shop_mean_features 피처는 제거했습니다.
시차 피처 생성 마무리 : 결측값 처리
지금까지 시차 피처를 만들었습니다. 모두 세 달 치까지 만들었죠. 자연스럽게 다음 그림처럼 월 ID가 0, 1, 2인 데이터에는 결측값이 생깁니다.
▼ 시차 피처 생성에 따른 결측값
결측값을 없애려면 월ID가 3 미만인 데이터를 제거해야 합니다.
1.6 피처 엔지니어링 VI : 기타 피처 엔지니어링
복잡한 과정은 모두 끝났습니다. 이제 간단한 피처 몇 개를 추가하고, 필요 없는 피처는 제거하겠습니다.
기타 피처 추가
그 외 추가할 만한 파생 피처는 또 무엇이 있을까요? 총 다섯 가지 피처를 더 추가하겠습니다.
월간 판매량 시차 피처들의 평균
먼저 월간 판매량 시차 피처들의 평균을 구해보겠습니다. 피처명은 ‘월간 판매량 시차평균’입니다.
간단하죠? 파생 피처를 만들 때 단순하게 사칙연산을 이용하기도 합니다. 기존 피처끼리 더하거나 빼거나 곱하거나 나눠서 새로운 피처를 만드는 겁니다.
이 피처도 판매량과 관련되어 있으니 값을 0~20 사이로 조정해야 합니다. 앞서 add_lag_features( ) 함수에서 값을 조정할 피처들을 lag_features_to_clip 리스트에 저장해뒀습니다. 이 리스트에 더해 타깃값인 월간 판매량과 방금 만든 월간 판매량 시차평균 피처를 0~20 사이로 조정하겠습니다. clip( ) 함수를 사용해서요.
시차 변화량
이번에는 다음과 같이 나누기 연산으로 시차 변화량 피처를 두 가지 만들겠습니다.
- 시차변화량1 = 월간 판매량_시차1 / 월간 판매량_시차2
- 시차변화량2 = 월간 판매량_시차2 / 월간 판매량_시차3
코드 중 replace([np.inf, -np.inf], np.nan).fillna(0) 부분은 값을 0으로 나누는 상황을 대처하는 방어 코드입니다. 양수를 0으로 나누면 무한대(np.inf)가 되고, 음수를 0으로 나누면 무한소(-np.inf)가 됩니다. 이런 경우 np.inf와 -np.inf를 0으로 바꾸는 일을 해주죠.
신상 여부
이번에는 신상품인지 여부를 나타내는 피처입니다. 첫 판매월이 현재 월과 같다면 신상품이겠죠. 이 로직을 이용해 간단하게 ‘신상여부’ 피처를 만들 수 있습니다.
첫 판매월과 월ID가 같으면 True, 다르면 False를 신상여부 피처에 추가했습니다.
첫 판매 후 경과 기간
현재 월에서 첫 판매월을 빼면 첫 판매 후 기간이 얼마나 지났는지 구할 수 있습니다. 이를 ‘첫 판매 후 기간’ 피처라고 하겠습니다.
월(month)
마지막으로 월 피처를 구해보겠습니다. 월ID 피처를 12로 나눈 나머지는 월과 같습니다.
이로써 다섯 가지 파생 피처를 다 만들었습니다.
필요 없는 피처 제거
지금까지 만든 피처 중 첫 판매월, 평균 판매가, 판매건수는 모델링에 필요가 없습니다. 이 피처들을 활용해 다른 파생 피처를 만들었죠. 첫 판매월은 ‘신상여부’, ‘첫 판매 후 기간’ 피처를 구하기에 쓰였고, 평균 판매가와 판매건수는 테스트 데이터에서 모두 0입니다. 그래서 이 세 피처는 제거하겠습니다.
마지막으로 다운캐스팅을 하여 메모리를 아껴줍니다.
1.7 피처 엔지니어링 VII : 마무리
길고 긴 피처 엔지니어링이 다 끝났습니다. info( ) 함수를 사용해 최종적으로 all_data에 어떤 피처가 있는지 살펴보겠습니다.
<class 'pandas.core.frame.DataFrame'>
Int64Index: 9904582 entries, 1122386 to 11026967
Data columns (total 31 columns):
# Column Dtype
--- ------ -----
0 월ID int8
1 상점ID int8
2 상품ID int16
3 월간 판매량 int8
4 도시 int8
5 상품분류ID int8
6 대분류 int8
7 월간 판매량_시차1 int8
8 월간 판매량_시차2 int8
9 월간 판매량_시차3 int8
10 판매건수_시차1 int8
11 판매건수_시차2 int8
12 판매건수_시차3 int8
13 평균 판매가_시차1 float32
14 평균 판매가_시차2 float32
15 평균 판매가_시차3 float32
16 상품ID별 평균 판매량_시차1 float32
17 상품ID별 평균 판매량_시차2 float32
18 상품ID별 평균 판매량_시차3 float32
19 상품ID 도시별 평균 판매량_시차1 float32
20 상품ID 도시별 평균 판매량_시차2 float32
21 상품ID 도시별 평균 판매량_시차3 float32
22 상점ID 상품분류ID별 평균 판매량_시차1 float32
23 상점ID 상품분류ID별 평균 판매량_시차2 float32
24 상점ID 상품분류ID별 평균 판매량_시차3 float32
25 월간 판매량 시차평균 float32
26 시차변화량1 float32
27 시차변화량2 float32
28 신상여부 int8
29 첫 판매 후 기간 int8
30 월 int8
dtypes: float32(15), int16(1), int8(15)
memory usage: 802.9 MB
총 31개 열이 있습니다. 이중 ‘월간 판매량’은 타깃값이고, 나머지 30개는 피처입니다.
💡 Warning: 다운캐스팅과 가비지 컬렉션을 하지 않으면 여기서 메모리 사용량이 2GB가 넘습니다. 그러면 모델 훈련부터는 메모리 사용량을 초과해 코드가 멈춥니다. 그렇기 때문에 이번 장에서는 다운캐스팅과 가비지 컬렉션이 꼭 필요합니다.
이제 all_data를 훈련, 검증, 테스트 데이터로 나누겠습니다. 베이스라인과 유사합니다. 가비지 컬렉션도 잊지 맙시다!
💡 Note: 베이스라인에서는 타깃값(월간 판매량)에 clip() 함수를 적용했지만 여기서는 따로 적용하지 않았습니다. 앞서 lag_features_to_clip에 저장된 피처와 월간 판매량, 월간 판매량 시차평균을 0~20 사이로 이미 조정했기 때문입니다.
1.8 모델 훈련 및 성능 검증
지금까지 다양한 피처 엔지니어링을 적용해 총 30개 피처를 손에 넣었습니다. 이 데이터로 모델을 훈련하고 예측하여 결과를 제출해보겠습니다.
모델 훈련 및 제출 흐름은 베이스라인과 비슷합니다. 하이퍼파라미터만 일부 다를 뿐입니다. 조기 종료 조건은 150번으로 설정했습니다. 더불어 범주형 데이터에는 상점ID와 상품분류ID 외에 도시, 대분류, 월을 추가했습니다.
[LightGBM] [Warning] Find whitespaces in feature_names, replace with underlines
[LightGBM] [Info] Total Bins 3886
[LightGBM] [Info] Number of data points in the train set: 9452298, number of used features: 30
[LightGBM] [Warning] Find whitespaces in feature_names, replace with underlines
[LightGBM] [Info] Start training from score 0.297707
Training until validation scores don't improve for 150 rounds
[100] training's rmse: 1.01082 valid_1's rmse: 0.987057
[200] training's rmse: 0.909234 valid_1's rmse: 0.923085
[300] training's rmse: 0.857869 valid_1's rmse: 0.898434
[400] training's rmse: 0.829775 valid_1's rmse: 0.888633
[500] training's rmse: 0.81135 valid_1's rmse: 0.884879
[600] training's rmse: 0.797562 valid_1's rmse: 0.884279
[700] training's rmse: 0.78753 valid_1's rmse: 0.884595
Early stopping, best iteration is:
[635] training's rmse: 0.793887 valid_1's rmse: 0.883926
800번째 이터레이션까지 결과를 출력하고 조기종료했습니다. 종료 시까지 성능이 가장 우수했을 때는 748번째 이터레이션이었으며, 이때 검증 데이터의 RMSE 값은 0.886313입니다. 베이스라인 모델에서는 1.00336이니 0.117047만큼 낮아졌습니다(RMSE 값은 낮을수록 좋습니다).
1.9 예측 및 결과 제출
훈련된 모델을 활용해 최종 예측하고 결과 파일을 만듭니다. 예측 값을 0~20 사이로 제한하는 것도 잊지 말아야겠죠?
가비지 컬렉션까지 해주죠.
커밋 후 제출해보겠습니다.
▼ 최종 점수
제출 결과 퍼블릭 점수는 0.87319입니다. 베이스라인 모델보다 0.20841만큼 좋아졌네요. 상위 4.2%입니다. 하지만 누차 설명했듯이 퍼블릭 점수는 크게 신경 쓸 필요 없습니다. 대회가 끝나면 프라이빗 점수가 공개되는데, 그러면 등수가 더 오를 수도 있고 떨어질 수도 있기 때문이죠. 이번 장의 실전 문제에서 성능 향상을 위한 팁을 소개했으니 꼭 참고해주세요.
2. 머신러닝 경진대회를 마치며
이번 장까지 머신러닝 모델을 활용한 네 가지 경진대회를 다뤘습니다. 6장에서는 캐글 경진대회 전반과, 베이스라인에서 시작해 모델 성능을 향상하는 일련의 절차를 배웠습니다. 몸풀기였죠.
7장에서는 범주형 데이터 문제를 다뤘습니다. 탐색적 데이터 분석을 자세히 다루며 데이터 특성에 따른 인코딩 방법을 배웠습니다.
8장에서는 많은 내용을 다뤘습니다. 우선 탐색적 데이터 분석으로 필요 없는 피처를 선별했죠. 게다가 캐글에서 가장 많이 쓰이는 XGBoost와 LightGBM 모델 사용법을 배웠습니다. 이외에도 OOF 예측, 베이지안 최적화, 앙상블 기법에 관해 배웠습니다.
이번 9장에서는 시계열 문제에서 활용할 수 있는 다양한 피처 엔지니어링 기법을 배웠습니다.
세세한 기법들이 더 많지만 이 정도만 확실하게 알아도 머신러닝 경진대회에 참가할 자격이 충분합니다. 상위권 캐글러의 코드를 참고하면서 경진대회에 참가하다 보면 피처 중요도, 스태킹 등의 다른 기법들도 익힐 수 있습니다. 지금까지 따라오시느라 고생 많으셨습니다. 다음 장부터는 딥러닝 경진대회를 다루겠습니다.
3. 학습 마무리
이번 장에서는 간단한 탐색적 데이터 분석 후 많은 시간을 피처 엔지니어링에 할애했습니다. 각 데이터 파일을 활용해 피처 엔지니어링을 수행하고, 시차 피처를 비롯한 여러 파생 피처를 만들었습니다. 최종 피처는 총 30개였습니다. LightGBM 모델로 퍼블릭 점수 0.87319를 기록했습니다.
3.1 핵심 요약
- 훈련 데이터가 여러 파일로 제공되면 공통 피처를 기준으로 병합해 사용합니다.
- 직접적인 타깃값이 제공되지 않기도 합니다. 이럴 때는 존재하는 피처를 조합하거나 계산하여 타깃값을 구합니다.
- 회귀문제에서는 특정 피처를 기준으로 데이터를 그룹화해 값을 집계해 사용하는 일이 많습니다. 집계 방법은 합, 평균, 중간값, 표준편차, 분산, 개수, 최솟값, 최댓값 등이 있습니다.
- 피처가 다양할 때는 피처명을 한글화하는 것도 좋은 방법입니다.
- 데이터가크면메모리관리도신경써야합니다.
- 데이터 다운캐스팅은 더 작은 데이터 타입으로 변환하는 작업을 말합니다.
- 가비지 컬렉션은 더는 사용하지 않는 영역을 해제하는 기능입니다.
- 이상치가 있을 때는 해당 데이터 자체를 제거하거나 적절한 값으로 바꿔줍니다.
- 둘 이상의 피처를 조합하면 유용한 데이터의 수가 늘어나는 효과가 있습니다.
- 분류 피처의 각 분류별 데이터 수가 적다면 대분류로 다시 묶어 훈련하는 것도 좋은 방법입니다.
- 시계열 데이터에서는 시간 흐름 자체가 중요한 정보입니다. OOF 예측이나 데이터를 무작위로 섞는 등 시간 순서를 무시하는 기법은 이용할 수 없습니다.
- 시차 피처란 과거 시점에 관한 피처로, 성능 향상에 도움되는 경우가 많아서 시계열 문제에서 자주 만드는 파생 피처입니다.