개인 스터디중 재밋고 쉬워보이는 대회가 있어서 참가하게 되었습니다. 대회는 시계열대회 예측이였으며 링크는 아래에 있습니다. 개인적으로 기간이 짧아서 아쉬움이 남긴 하지만, 코드를 공유하며 정리했던 생각을 남겨둘려 합니다.
STEP 1. Understanding the Problem
- 시계열 예측 문제로, 쇼핑몰에서 모은 시계열 데이터를 이용하여 다음 주 매출 (Weekly_Sales)을 예측하는 문제입니다. 데이터는 아래와 같이 구성되어 있습니다.
- id : 샘플 아이디
- Store : 쇼핑몰 지점
- Date : 주 단위(Weekly) 날짜
- Temperature : 해당 쇼핑몰 주변 기온
- Fuel_Price : 해당 쇼핑몰 주변 연료 가격
- Promotion 1~5 : 해당 쇼핑몰의 비식별화된 프로모션 정보
- Unemployment : 해당 쇼핑몰 지역의 실업률
- IsHoliday : 해당 기간의 공휴일 포함 여부
- Weekly_Sales : 주간 매출액 (목표 예측값)
시계열 예측을 위하여 몇가지 전략을 세워봤으며 그 전략들은 아래와 같습니다.
1. 전통적인 시계열 예측방법 (ARIMA, SARIMA) 등을 이용하여 추세 예측해보기
2. 딥러닝을 사용하여 예측해보기
3. ML만을 사용하여 예측해보기
4. 위 모델들 앙상블하기
위 전략들을 세웠을 때, 앙상블이 가장 높은 정확도를 나타내어 앙상블 모델에 대한 코드를 정리하려 합니다.
train = pd.read_csv("../train.csv", index_col=0)
test = pd.read_csv("../test.csv", index_col=0)
train.head()
STEP 2. Data Cleaning
먼저 Boolean인 휴일정보와 날짜정보를 수정해 주겠습니다.
def holiday_to_number(isholiday):
if isholiday == True: # 휴일일경우 1
number = 1
else:
number = 0
return number
def preprocessing(data):
data = data.copy()
# 날짜 형식에 대해 데이터타입 적용
data.Date = pd.to_datetime(data.Date, format="%d/%m/%Y")
# 날짜 데이터를 주, 일, 월 달 로 분리
data['Week'] = data.Date.dt.isocalendar().week.apply(lambda x: int(x))
data['Day'] = data.Date.dt.day.apply(lambda x: int(x))
data['Year'] = data.Date.dt.year.apply(lambda x: int(x))
data['Month'] = data.Date.dt.month.apply(lambda x: int(x))
# True/False => 1/0
data['NumberHoliday'] = data['IsHoliday'].apply(holiday_to_number)
return data
train = preprocessing(train)
test = preprocessing(test)
STEP 3. Data Analysis
- Store 별 판매량 그려보기
fig = plt.figure(figsize=(30,60))
train_df = train
for store in range(1,46):
storeset = train_df[train_df.Store==store]
storeset_2010 = storeset[(storeset.Year==2010) & (storeset.Week<=43)]
storeset_2011 = storeset[(storeset.Year==2011) & (storeset.Week<=43)]
storeset_2012 = storeset[(storeset.Year==2012) & (storeset.Week<=43)]
ax = fig.add_subplot(12, 4, store)
plt.title(f"store_{store}")
ax.plot(storeset_2010.Week, storeset_2010.Weekly_Sales, label="2010", alpha=0.3)
ax.plot(storeset_2011.Week, storeset_2011.Weekly_Sales, label="2011", alpha=0.3)
ax.plot(storeset_2012.Week, storeset_2012.Weekly_Sales, label="2012", color='r')
#ax.plot(test_pred_store.Week, test_pred_store.Before_Weekly_Sales, label="2012-pred", color='b')
ax.legend()
plt.show()
위 그래프에서 확인할 수 있듯이, 대부분 특정일자에 크게 판매량이 증가하는 증상을 보이며, 몇몇 점포들 제외하고선 비슷한 판매양상을 보입니다. 먼저 추세를 확인하기 위해, Arima 모델을 사용하여 각 Store 별로 Hierarchical Model 을 생성 할 생각이며, 그 이후 유의미한 Feature 들을 사용하여 XGboost 를 사용하여 각각 스토어별 모델을 만들고 앙상블로 Arima 의 시계열 추세와 XGBoost 모델을 합쳐줄 생각입니다.
STEP 4. ARIMA
# 필요한 Library Import
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import adfuller
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from pandas.plotting import autocorrelation_plot
import statsmodels.api as sm
from sklearn.metrics import mean_squared_error
df = pd.read_csv("../train.csv", index_col = 'Date')
#Stationary Check Using ADF
result = adfuller(df[df.Store==1].Weekly_Sales)
print('ADF Statistic: %f' % result[0])
print('p-value: %f' % result[1])
p-value 가 0.05보다 작으므로 Store 1의 데이터는 Stationary 하다고 볼 수 있다. 하여 d = 0 으로 두고 학습해도 된다.
스토어가 총 45개인것에 비해 데이터가 많지않으므로 그냥 0~3 사이의 모든 정수를 하이퍼파라미터로 두고 Grid search 학습 방법을 택하기로 정했다. 1개의 Store의 시계열만 볼 경우 ADF, AR 등을 직접 보고 골라가는게 좋을수 있지만, 45개의 Store 을 다 보기에는 시간이 걸리기 때문이다. 먼저 Arima를 학습하는 Function 을 만들어 주겠습니다.
def train_arima(df, store, col, p, d, q):
df = df[df['Store'] == store]
model = ARIMA(df[col], order=(p,d,q))
results_ARIMA = model.fit()
return results_ARIMA
Train Test Split:
- (각 스토어별 데이터수는 총 139 개인데 그 중 125개를 학습, 14개를 테스트 데이터로 나누어 주었습니다.)
train_size = int(len(df[df['Store'] == 1])*.9)
test_size = len(df[df['Store'] == 1]) - train_size
print(f'train: {train_size}, test: {test_size}') # train: 125, test: 14
def hierarchical_train_test_split():
train_list = []
test_list = []
for i in range(1,max(df.Store)+1):
train = df[df.Store==i].iloc[:train_size]
test = df[df.Store==i].iloc[train_size:]
train_list.append(train)
test_list.append(test)
train_list = pd.concat(train_list)
test_list = pd.concat(test_list)
return train_list, test_list
train, test = hierarchical_train_test_split()
# Grid Search
p_values = range(0,4)
d_values = range(0,1)
q_values = range(0,4)
# grid_dict = {}
grid_list = []
for store in range(1, max(df.Store)+1):
for p in p_values:
for d in d_values:
for q in q_values:
warnings.filterwarnings("ignore")
pred = train_arima(train, store, 'Weekly_Sales', p,d,q).forecast(14)
rmse = mean_squared_error(pred, test[test.Store==store].Weekly_Sales, squared=False)
grid_list.append([store,p,d,q,rmse])
print(f'{store}: {p},{d},{q},{rmse}')
위와 같이 모든 가능한 수를 넣어주게 되면, 각 RMSE를 계산해서 Return 해주게 만들어 보았다. 이제 각 Store 별 RMSE 가 가장 낮은 p,d,q만 따로 뽑아보겠다.
grid_np = np.array(grid_list).reshape(45,16,5) # shape 을 스토어별로 바꾸어줌
rmse_min = []
for i in range(len(grid_np)):
idx = np.argmin(grid_np[i], axis=0)[-1] # return last value minimum array
rmse_min.append(grid_np[i][idx])
이제 나눈데이터 말고 실제 경진대회 Train data로 모델을 fit 하고 fitted value 와 test value 를 저장하겠다.
# train and test for real_data
X = pd.read_csv(r"C:\Users\Eric\Downloads\dataset\train.csv")
y = pd.read_csv(r"C:\Users\Eric\Downloads\dataset\test.csv")
sales_pred = []
sales_forecast = []
for i in range(0, max(X.Store)):
store, p,d,q,rmse = rmse_min[i]
print(f'Store: {store}, {p}, {d}, {q}')
pred = train_arima(X, store, 'Weekly_Sales', int(p),int(d),int(q)).fittedvalues
forecast = train_arima(X, store, 'Weekly_Sales', int(p),int(d),int(q)).forecast(4)
sales_pred.append(pred)
sales_forecast.append(forecast)
# Fitted value 저장
arima_pred = pd.DataFrame(np.array(sales_pred).reshape(-1), columns=['Weekly_Sales'])
arima_pred.index +=1
arima_pred.index.name = 'id'
arima_pred.to_csv('../arima_fittedvalue')
# forecast 저장
arima_pred = pd.DataFrame(np.array(sales_forecast).reshape(-1), columns=['Weekly_Sales'])
arima_pred.index +=1
arima_pred.index.name = 'id'
arima_pred.to_csv('../arima_forecast')
STEP 5. Feature Selection
유의미한 Feature를 사용하기위해 이 글의 방법을 따라하였습니다. https://mojitos.tistory.com/75
[논문분석] 머신러닝에서 유의미한 Feature 쉽게 구분해내기
참고: https://www.jmlr.org/papers/volume3/stoppiglia03a/stoppiglia03a.pdf [서론] 빅데이터를 다루다보면 많은 feature들을 마주하게 됩니다. 많은 feature 들이 다 유의미한 값을 가져서 target값을 예측하..
mojitos.tistory.com
위 방법으로 Feature 를 살펴 보았을때, Temperature 및 Fuel Price 을 빼는것이 차원을 좀더 낮추고 좋은 결과를 얻을 수 있다는것을 찾아냈습니다. 그리하여, 몇개의 Feature 만 사용하여 XGBoost를 년도별로 진행하여 보았습니다.
import xgboost
features = ['Store', 'Promotion1', 'Promotion2', 'Promotion3',
'Promotion4', 'Promotion5', 'Unemployment',
'Week', 'Day','Month', 'Year', 'NumberHoliday']
# 데이터 전처리
trainset = preprocessing(train)
testset = preprocessing(test)
STEP 6. ML
XGBoost Train 하는 부분은 공유해주신 코드를 참조하였습니다.
models = []
models_pred = []
model_params = {
"n_estimators": 60,
"min_child_weight": 3,
"max_depth": 6
}
for store in range(1,max(trainset.Store)+1):
train_store = trainset[trainset.Store==store]
# 2010, 2011, 2012 년도 별로 데이터 분리
# 2012-09에 대해 예측하려고 하기 때문에 2012년도는 9월을 포함하지 않음
train_store_2010 = train_store[(train_store.Year==2010) & (train_store.Month<=9)]
train_store_2011 = train_store[(train_store.Year==2011) & (train_store.Month<=9)]
train_store_2012 = train_store[(train_store.Year==2012) & (train_store.Month<9)]
# 2011, 2010 년도를 제외한 데이터 생성
train_store_2010_2012 = pd.concat([train_store_2010, train_store_2012])
train_store_2011_2012 = pd.concat([train_store_2011, train_store_2012])
# 각각의 모델 학습
model_2010_2012 = xgboost.XGBRegressor(**model_params)
model_2010_2012.fit(train_store_2010_2012[features],
train_store_2010_2012.Weekly_Sales)
model_2011_2012 = xgboost.XGBRegressor(**model_params)
model_2011_2012.fit(train_store_2011_2012[features],
train_store_2011_2012.Weekly_Sales)
# 2012년도 9월에 대해서 예측
x_test = train_store[(train_store.Year==2012) & (train_store.Month==9)]
pred_2010_2012 = model_2010_2012.predict(x_test[features])
pred_2011_2012 = model_2011_2012.predict(x_test[features])
# 예측 결과 평가
rmse_2010_2012 = mean_squared_error(pred_2010_2012, x_test.Weekly_Sales, squared=False)
rmse_2011_2012 = mean_squared_error(pred_2011_2012, x_test.Weekly_Sales, squared=False)
# 오차가 더 적은 연도를 선택
similar_year = 2010
if rmse_2010_2012 > rmse_2011_2012:
similar_year = 2011
print(f"{store:02}", similar_year, rmse_2010_2012, rmse_2011_2012)
# 전체 데이터로 학습
train_store_target = pd.concat([
train_store[(train_store.Year==similar_year) & (train_store.Month<=10)],
train_store[(train_store.Year==2012)]
])
model = xgboost.XGBRegressor(**model_params)
model.fit(train_store_target[features], train_store_target.Weekly_Sales)
models_pred.append(model.predict(train_store[features]))
models.append(model)
# prediction
model_pred = pd.DataFrame(np.array(models_pred).reshape(-1), columns=['Weekly_Sales'])
model_pred.index += 1
model_pred.index.name = 'id'
model_pred.to_csv('../xgboost_fittedvalue.csv')
# Forecast
pred = []
for store in range(1, max(trainset.Store)+1):
test_store = testset[testset.Store==store]
y = models[store-1].predict(test_store[features])
pred += y.tolist()
model_pred = pd.DataFrame(np.array(pred).reshape(-1), columns=['Weekly_Sales'])
model_pred.index += 1
model_pred.index.name = 'id'
# model_pred.to_csv('../xgboost_forecast.csv')
STEP 7. Test
마지막으로 두가지 모델을 Linear Regression 모델로 Ensemble 모델을 만들어 줄것입니다.
관련 논문 참조: https://arxiv.org/pdf/2201.02034.pdf
from sklearn.linear_model import LinearRegression
pred_all = []
store = 1
start_col = 0
end_col = 139
test_start_col = 0
test_end_col = 4
while store <= 45:
lr = LinearRegression()
lr.fit(trainX[start_col:end_col], prophet[start_col:end_col].Weekly_Sales)
final_pred = lr.predict(test[test_start_col:test_end_col])
pred_all.append(final_pred)
print(f'{store}, {start_col}, {end_col}, {test_start_col}, {test_end_col}')
start_col += 139
end_col += 139
store += 1
test_start_col += 4
test_end_col += 4
각 스토어 별로 마지막 데이터 후 4주씩 예측을 해줍니다. 그 후 결과를 저장해줍니다.
model_pred = pd.DataFrame(np.array(pred_all).reshape(-1), columns=['Weekly_Sales'])
model_pred.index += 1
model_pred.index.name = 'id'
model_pred.to_csv('../XgboostArima.csv')
Private 약 40000대 RMSE로 약 4X등의 결과를 거두었습니다.
짧은기간동안 데이터 분석보단 기술적으로 모델의 성능을 높이는데 집중을 하였습니다.
관련 공부했던 논문들을 적용시켜볼 기회가 되어서 좋은 경험이였습니다.
[결론]
시계열 데이터에서 Prophet, Arima, Sarima등의 전통 분석 방법을 사용하면 시계열 추세를 잘 따라주긴 하나 이상치들을 예측하는데는 어려움이 있었습니다. 이를 위해, XGBoost, LightGBM등 각종 다른 모델들을 사용하여 이상치를 잡아내기 위해 노력하였고, 그중 가장 성능이 좋았던 XGBoost 를 사용하여 Arima(추세) + XGBoost(이상치) 의 Ensemble 모델을 고안해 보았습니다. 결과는 각각의 모델들보다 성능이 더 띄어났으며, 데이터의 프로모션등 다른 Nan값들의 전처리 방법이 충분히 고안된다면 위 모델을 사용하여 더 좋은 성능을 내는 모델을 만들 수 있을것같습니다.
대회: https://dacon.io/competitions/official/235942/overview/description
데이터: https://drive.google.com/file/d/1TEwm7wxyviepHUDe81Eqzwy_g0tPXuNh/view
'2. Data Science Basics > Python' 카테고리의 다른 글
Python 스크립트 폴더 정리방법 (0) | 2023.11.27 |
---|---|
[논문분석] 머신러닝에서 유의미한 Feature 쉽게 구분해내기 (0) | 2022.05.30 |
Dynamic Time Warping을 이용하여 비슷한 주식 clustering 하기 (1) | 2022.03.03 |
Dynamic Time Warping (DTW) (0) | 2022.03.02 |
Python 을 이용하여 MDD / Sharp Ratio 구하기 (0) | 2022.01.03 |
댓글