Random Forest로 시계열데이터 예측하기

지금 회사에서 가장 많이 한 업무 중 하나가 바로 주문수 예측이었다.

처음에 단순히 곱셈으로 작업하던 부분을 개선해서 이전보다 좀 더 세밀히 하기는 하지만 여전히 정확도 측면에서는 개선할 부분이 많아 보이고 이와 관련된 방법을 찾으려고 노력하였다.

KPI는 기본 적으로 여러 부서의 성과를 종합한 최종 수치이기 때문에 여러 변수들의 결합으로 표현할 수 있어야 한다. 오늘 다룰 내용은 RandomForest 를 이용한 KPI 예측이다.

Random Forest

랜덤 포레스트(Random Forest 이하 RF)는 기본적으로 배깅(Bagging)의 확장판이다. 배깅은 의사결정나무의 확장판으로 회귀 및 분류 문제에서 널리 쓰이는 방법이다.

배깅은 의사결정나무를 앙상블 형태로 조합한 것으로 부트스트래핑(Bootstrapping)을 활용하였기 때문에 각각 다른 데이터 셋을 가지고 의사결정나무를 적합한 후에 회귀에서는 여러 의사결정나무의 평균치를 사용하고, 분류문제에서는 다수의 표를 얻은 예측치를 사용해서 문제를 해결한다.

RF가 배깅과의 차이점이 있다면 입력시점에 훈련데이터의 모든 컬럼을 사용하지 않고 Subset을 만들어서 훈련한다는 것이다. 의사결정나무를 적합할 때 어떤 변수를 가지고 데이터를 분할할지 각각의 입력변수를 평가하는데 비슷한 컨셉을 적용한 것이라고 볼 수 있다. RF는 각각의 의사결정트리는 서로 다른 변수들을 가지고 분할점을 설정했을 것이기 때문이다.

데이터 준비

통상 시계열 데이터는 시점(Time point)와 그 시점의 수치로 이루어진다. 그런데 RF는 지도학습의 형태이기 때문에 문제를 풀기 위해서는 데이터를 Feature와 Label의 형태로 변경해줄 필요가 있다. 예를 들어 다음과 데이터가 있다고 하자.

2020-10-01100
2020-10-02115
2020-10-03105
2020-10-04109

<Table 1>

그러면 해당 데이터를 다음과 같이 변경해줄 수 있다.

NA100
100115
115105
105109

<Table 2>

위와 같이 과거의 데이터를 하나씩 Shift해주는 것이다. 여기까지는 큰 문제가 아니다. 사실 데이터를 사용할 때가 문제이다. 시계열 데이터는 보통 Trend, Seasonality, Cycle, Remainder의 조합으로 분해할 수 있다고 하는데, 이 부분을 생각한다면 기존의 정형데이터처럼 Validation을 할 수가 없다. 미래의 데이터로 학습해서 과거를 예측하는 것이 불가하다는 의미이다. K-Fold Validation역시 사용할 수 없다. 변환하는 코드는 다음과 같다.

# transform a time series dataset into a supervised learning dataset
def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
	n_vars = 1 if type(data) is list else data.shape[1]
	df = DataFrame(data)
	cols = list()
	# input sequence (t-n, ... t-1)
	for i in range(n_in, 0, -1):
		cols.append(df.shift(i))
	# forecast sequence (t, t+1, ... t+n)
	for i in range(0, n_out):
		cols.append(df.shift(-i))
	# put it all together
	agg = concat(cols, axis=1)
	# drop rows with NaN values
	if dropnan:
		agg.dropna(inplace=True)
	return agg.values

대신 walk-forward validation이라는 기법을 사용한다. 해당 기법은 이후에 별도로 다뤄보겠지만, 간단하게 이야기 하면 최근 12개월 정도 데이터는 훈련시 제외해놓고 이후에 Testing의 용도로 최근 12개월 데이터를 사용해서 검증해보는 것을 말한다.

모델 학습하기

앞서 언급한 Walk-Forward Validation을 하는 코드는 다음과 같다. 대략 코드를 보면 For문으로 데이터를 돌면서 매시점 데이터를 예측하고 예측한 데이터를 다시 하나의 입력데이터로 사용해서 예측하는 것을 볼 수 있다.

# walk-forward validation for univariate data
def walk_forward_validation(data, n_test):
	predictions = list()
	# split dataset
	train, test = train_test_split(data, n_test)
	# seed history with training dataset
	history = [x for x in train]
	# step over each time-step in the test set
	for i in range(len(test)):
		# split test row into input and output columns
		testX, testy = test[i, :-1], test[i, -1]
		# fit model on history and make a prediction
		yhat = random_forest_forecast(history, testX)
		# store forecast in list of predictions
		predictions.append(yhat)
		# add actual observation to history for the next loop
		history.append(test[i])
		# summarize progress
		print('>expected=%.1f, predicted=%.1f' % (testy, yhat))
	# estimate prediction error
	error = mean_absolute_error(test[:, -1], predictions)
	return error, test[:, 1], predictions

앞서 언급한 시계열 특성을 고려하면서 Data Split을 하기 위한 함수는 다음과 같다. 데이터를 특정 시점 전후로 나눠서 보는 것을 확인할 수 있다.

# split a univariate dataset into train/test sets def train_test_split(data, n_test): return data[:-n_test, :], data[-n_test:, :]

마지막으로 RF 함수는 다음과 같다. 사실 별도 함수로 Wrapping 하긴 했지만 내부를 보면 Split을 해주고 RandomForestRegressor을 불러와서 한 스텝 앞 시점을 예측하고 예측한 값을 알려주는 게 다이다.

# fit an random forest model and make a one step prediction
def random_forest_forecast(train, testX):
	# transform list into array
	train = asarray(train)
	# split into input and output columns
	trainX, trainy = train[:, :-1], train[:, -1]
	# fit model
	model = RandomForestRegressor(n_estimators=1000)
	model.fit(trainX, trainy)
	# make a one-step prediction
	yhat = model.predict([testX])
	return yhat[0]

결론

전통적인 시계열 예측 모델은 과거의 기록은 결국 미래에 어떠한 형식으로 영향을 미칠지에 대해서 가정하는 형식으로 발전해왔다. 그 중에서 항상 전제가 되는 것 중 하나는 바로 정상성(Stationary)이었다. 하지만 이 정상성이 실생활에서는 참 적용되기 어려운 부분이다. 차분으로 하거나 Log를 씌우는 등의 다양한 변환을 해도 이론에 딱 맞춰서 만들기 어렵다. 그리고 Dynamic Regression 등 시계열에 영향을 미치는 다양한 변수를 포함하기 위한 기법이 있지만 여전히 어렵다. 이러한 기법도 점차 고도화되서 X-12 Arima등 국가 정책에 뒷받침되는 근거로 사용 되고 있지만 실무에서 적용하기에는 많은 수고로움이 있는 것을 감안한다면 RF를 통한 예측은 괜찮은 선택지 중 하나라고 생각한다.

하지만 RF 특성상 Overfitting을 피하기 위해 변수를 임의로 선택해서 적합하는 부분등의 이슈는 시계열에서 적용하기가 다소 어려워 보이지 않나라는 생각이 들고 그 외에도 변수를 많이 입력함으로써 ARIMA가 가졌던 한계점을 보완할 수는 있지만 궁극적으로 해결하는 형태는 아닌지라 보조용 정도로 쓰면 적합하지 않나라는 생각이 든다.

footnote: How To Backtest Machine Learning Models for Time Series Forecasting