Интуитивное использование сезонности для повышения точности модели
Добро пожаловать во вторую часть анализа временных рядов! В этом посте мы будем работать над моделированием данных временных рядов. Это продолжение моей предыдущей публикации о данных временных рядов.
В нашем предыдущем сообщении в блоге мы говорили о том, что такое данные временных рядов, как отформатировать такие данные, чтобы максимизировать их полезность, и как обрабатывать недостающие данные. Мы также узнали, как пересчитывать данные временных рядов по месяцам, неделям, годам и т. Д. И вычислять скользящие средние. Мы углубились в такие концепции, как тренды, сезонность, дифференциация первого порядка и автокорреляция. Если вы знакомы с большинством вещей, то все готово. Если вам нужно освежиться, вы можете выполнить быстрый поиск в Google по этим темам или прочитать мой предыдущий пост здесь.
Несколько слов перед тем, как мы начнем:
Существуют и другие, несомненно, лучшие пакеты, доступные для прогнозирования временных рядов, такие как ARIMA или собственное программное обеспечение Facebook Prophet. Однако эта статья была вдохновлена домашним заданием подруги, которое требовало, чтобы она использовала только Scikit, Numpy и Pandas (или столкнулась с мгновенной дисквалификацией!).
Давайте погрузимся в наш набор данных
Мы будем работать с общедоступным набором данных Open Power System Data. Вы можете скачать данные здесь. Он включает потребление электроэнергии, производство энергии ветра и производство солнечной энергии за 2006–2017 годы.
url='https://raw.githubusercontent.com/jenfly/opsd/master/opsd_germany_daily.csv' data = pd.read_csv(url,sep=",")
После установки столбца Date
в качестве индекса наш набор данных выглядит так:
# to explicitly convert the date column to type DATETIME data['Date'] = pd.to_datetime(data['Date']) data = data.set_index('Date')
Определение задачи моделирования
Цели предсказания
Наша цель - предсказать Consumption
(в идеале для будущих невидимых дат) на основе этого набора данных временного ряда.
Набор для обучения и тестирования
Мы будем использовать данные за 10 лет для обучения, то есть за 2006–2016 годы, и данные за прошлый год для тестирования, то есть за 2017 год.
Показатель производительности
Чтобы оценить, насколько хороша наша модель, мы будем использовать R-квадрат и среднеквадратичную ошибку (но будем печатать все соответствующие показатели, чтобы вы могли принять окончательное решение).
Вспомогательные функции
Чтобы распечатать все показатели производительности, относящиеся к задаче регрессии (например, MAE и R-квадрат), мы будем определять функцию regression_results
.
import sklearn.metrics as metrics def regression_results(y_true, y_pred): # Regression metrics explained_variance=metrics.explained_variance_score(y_true, y_pred) mean_absolute_error=metrics.mean_absolute_error(y_true, y_pred) mse=metrics.mean_squared_error(y_true, y_pred) mean_squared_log_error=metrics.mean_squared_log_error(y_true, y_pred) median_absolute_error=metrics.median_absolute_error(y_true, y_pred) r2=metrics.r2_score(y_true, y_pred) print('explained_variance: ', round(explained_variance,4)) print('mean_squared_log_error: ', round(mean_squared_log_error,4)) print('r2: ', round(r2,4)) print('MAE: ', round(mean_absolute_error,4)) print('MSE: ', round(mse,4)) print('RMSE: ', round(np.sqrt(mse),4))
Функциональная инженерия
В качестве основы мы выбираем упрощенную модель, которая прогнозирует сегодняшнюю стоимость потребления на основе
- вчерашнее значение потребления и;
- разница между вчерашним и позавчерашним значениями потребления.
# creating new dataframe from consumption column data_consumption = data[['Consumption']] # inserting new column with yesterday's consumption values data_consumption.loc[:,'Yesterday'] = data_consumption.loc[:,'Consumption'].shift() # inserting another column with difference between yesterday and day before yesterday's consumption values. data_consumption.loc[:,'Yesterday_Diff'] = data_consumption.loc[:,'Yesterday'].diff() # dropping NAs data_consumption = data_consumption.dropna()
Определение обучающих и тестовых наборов
X_train = data_consumption[:'2016'].drop(['Consumption'], axis = 1) y_train = data_consumption.loc[:'2016', 'Consumption'] X_test = data_consumption['2017'].drop(['Consumption'], axis = 1) y_test = data_consumption.loc['2017', 'Consumption']
Перекрестная проверка данных временных рядов
Во время собеседований по науке о данных часто возникает вопрос: Какой метод перекрестной проверки вы бы использовали для данных временных рядов?
У вас может возникнуть соблазн склониться к самой популярной кросс-валидации K-Fold (поверьте мне, до недавнего времени - не спрашивайте, сколько времени! - я не знал, что существуют другие методы CV, кроме K-fold). К сожалению, это был бы неправильный ответ. Причина в том, что он не принимает во внимание, что данные временных рядов имеют некоторый естественный порядок, а рандомизация при стандартной k-кратной перекрестной проверке не сохраняет этот порядок.
Лучшей альтернативой для перекрестной проверки данных временных рядов (чем K-кратное CV) является стратегия Forward Chaining.
В прямой цепочке, скажем, с 3 сгибами, наборы поездов и валидации выглядят так:
- кратность 1: обучение [1], проверка [2]
- кратность 2: обучение [1 2], проверка [3]
- кратность 3: обучение [1 2 3], проверка [4]
где 1, 2, 3, 4 представляют год. Таким образом, последовательные тренировочные наборы являются надмножествами предшествующих.
К счастью для нас, в sklearn есть возможность реализовать такое разделение теста поездов с использованием TimeSeriesSplit
.
from sklearn.model_selection import TimeSeriesSplit
Функция TimeSerieSplit
принимает в качестве входных данных количество разделений. Поскольку наши данные обучения имеют 11 уникальных лет (2006-2016), мы должны установить n_splits = 10
. Таким образом, у нас есть аккуратные наборы для обучения и проверки:
- кратность 1: обучение [2006], валидация [2007]
- кратное 2: обучение [2006 2007], валидация [2008]
- тройка: обучение [2006 2007 2008], валидация [2009]
- 4 раза: обучение [2006 2007 2008 2009], валидация [2010]
- раз 5: обучение [2006 2007 2008 2009 2010], валидация [2011]
- раз 6: обучение [2006 2007 2008 2009 2010 2011], валидация [2012]
- раз 7: обучение [2006 2007 2008 2009 2010 2011 2012], проверка [2013]
- кратная 8: обучение [2006 2007 2008 2009 2010 2011 2012 2013], проверка [2014]
- раз 9: обучение [2006 2007 2008 2009 2010 2011 2012 2013 2014], проверка [2015]
- кратная 10: обучение [2006 2007 2008 2009 2010 2011 2012 2013 2014 2015], проверка [2016]
Алгоритмы выборочной проверки
# Spot Check Algorithms models = [] models.append(('LR', LinearRegression())) models.append(('NN', MLPRegressor(solver = 'lbfgs'))) #neural network models.append(('KNN', KNeighborsRegressor())) models.append(('RF', RandomForestRegressor(n_estimators = 10))) # Ensemble method - collection of many decision trees models.append(('SVR', SVR(gamma='auto'))) # kernel = linear # Evaluate each model in turn results = [] names = [] for name, model in models: # TimeSeries Cross validation tscv = TimeSeriesSplit(n_splits=10) cv_results = cross_val_score(model, X_train, y_train, cv=tscv, scoring='r2') results.append(cv_results) names.append(name) print('%s: %f (%f)' % (name, cv_results.mean(), cv_results.std())) # Compare Algorithms plt.boxplot(results, labels=names) plt.title('Algorithm Comparison') plt.show()
И KNN, и RF работают одинаково хорошо. Но я лично предпочитаю RF, поскольку эта ансамблевая модель (объединяет несколько «индивидуальных» (разнообразных) моделей вместе и обеспечивает превосходную мощность прогнозирования.) Может работать практически сразу же, и это одна из причин, почему они очень популярны.
Гиперпараметры поиска в сетке
О необходимости поиска по сетке гиперпараметров я рассказывал в одной из своих предыдущих статей.
Оптимальная комбинация гиперпараметров максимизирует производительность модели, не приводя к проблеме высокой дисперсии (переобучения).
Код Python для выполнения поиска по сетке выглядит следующим образом:
from sklearn.model_selection import GridSearchCV model = RandomForestRegressor() param_search = { 'n_estimators': [20, 50, 100], 'max_features': ['auto', 'sqrt', 'log2'], 'max_depth' : [i for i in range(5,15)] } tscv = TimeSeriesSplit(n_splits=10) gsearch = GridSearchCV(estimator=model, cv=tscv, param_grid=param_search, scoring = rmse_score) gsearch.fit(X_train, y_train) best_score = gsearch.best_score_ best_model = gsearch.best_estimator_
Если вы заметили приведенный выше код, мы определили настраиваемый счетчик, установив scoring = rmse_score
вместо использования одной из общих показателей оценки, определенных в sklearn. Мы определяем нашего индивидуального счетчика следующим образом:
from sklearn.metrics import make_scorer def rmse(actual, predict): predict = np.array(predict) actual = np.array(actual) distance = predict - actual square_distance = distance ** 2 mean_square_distance = square_distance.mean() score = np.sqrt(mean_square_distance) return score rmse_score = make_scorer(rmse, greater_is_better = False)
Проверка наилучшей производительности модели на тестовых данных
y_true = y_test.values y_pred = best_model.predict(X_test) regression_results(y_true, y_pred)
Для начала это неплохо. Посмотрим, сможем ли мы улучшить нашу модель.
Возврат при проектировании функций
До сих пор мы использовали значения на (t-1)th
день для прогнозирования значений на t
день. Теперь давайте также будем использовать значения от (t-2)
days для прогнозирования потребления:
# creating copy of original dataframe data_consumption_2o = data_consumption.copy() # inserting column with yesterday-1 values data_consumption_2o['Yesterday-1'] = data_consumption_2o['Yesterday'].shift() # inserting column with difference in yesterday-1 and yesterday-2 values. data_consumption_2o['Yesterday-1_Diff'] = data_consumption_2o['Yesterday-1'].diff() # dropping NAs data_consumption_2o = data_consumption_2o.dropna()
Сброс поезда и тестового набора
X_train_2o = data_consumption_2o[:'2016'].drop(['Consumption'], axis = 1) y_train_2o = data_consumption_2o.loc[:'2016', 'Consumption'] X_test = data_consumption_2o['2017'].drop(['Consumption'], axis = 1) y_test = data_consumption_2o.loc['2017', 'Consumption']
Проверка того, работает ли «лучший» случайный лес с использованием «новых» предикторов лучше
model = RandomForestRegressor() param_search = { 'n_estimators': [20, 50, 100], 'max_features': ['auto', 'sqrt', 'log2'], 'max_depth' : [i for i in range(5,15)] } tscv = TimeSeriesSplit(n_splits=10) gsearch = GridSearchCV(estimator=model, cv=tscv, param_grid=param_search, scoring = rmse_score) gsearch.fit(X_train_2o, y_train_2o) best_score = gsearch.best_score_ best_model = gsearch.best_estimator_ y_true = y_test.values y_pred = best_model.predict(X_test) regression_results(y_true, y_pred)
Отличные новости!! Мы значительно снизили значения RMSE и MAE, в то время как значение R-квадрат также выросло.
Feature Engineering наносит ответный удар
Давайте посмотрим, полезно ли добавление стоимости производства солнечной энергии в некотором роде для прогнозирования потребления электроэнергии.
data_consumption_2o_solar = data_consumption_2o.join(data[['Solar']]) data_consumption_2o_solar = data_consumption_2o_solar.dropna()
Сброс Train / Test + GridSearch + Проверка работоспособности
X_train_2o_solar = data_consumption_2o_solar[:'2016'].drop(['Consumption'], axis = 1) y_train_2o_solar = data_consumption_2o_solar.loc[:'2016', 'Consumption'] X_test = data_consumption_2o_solar['2017'].drop(['Consumption'], axis = 1) y_test = data_consumption_2o_solar.loc['2017', 'Consumption'] model = RandomForestRegressor() param_search = { 'n_estimators': [20, 50, 100], 'max_features': ['auto', 'sqrt', 'log2'], 'max_depth' : [i for i in range(5,15)] } tscv = TimeSeriesSplit(n_splits=5) gsearch = GridSearchCV(estimator=model, cv=tscv, param_grid=param_search, scoring = rmse_score) gsearch.fit(X_train_2o_solar, y_train_2o_solar) best_score = gsearch.best_score_ best_model = gsearch.best_estimator_ y_true = y_test.values y_pred = best_model.predict(X_test) regression_results(y_true, y_pred)
Вуаля, производительность модели теперь даже лучше.
График переменной важности
imp = best_model.feature_importances_ features = X_train_2o_solar.columns indices = np.argsort(imp) plt.title('Feature Importances') plt.barh(range(len(indices)), imp[indices], color='b', align='center') plt.yticks(range(len(indices)), [features[i] for i in indices]) plt.xlabel('Relative Importance') plt.show()
Как мы видим, количество солнечной энергии не является таким сильным предсказателем энергопотребления, как другие временные предсказатели.
Финал Feature Engineering
Если вы следили за повествованием в Части 1 моего предыдущего сообщения в блоге, вы бы вспомнили, что в нашем наборе данных есть некоторый сезонный элемент, а точнее еженедельная сезонность. Таким образом, было бы разумнее использовать в качестве входных данных для модели значение потребления за неделю до указанной даты. Это означает, что если модель пытается спрогнозировать значение потребления на 8 января, ей необходимо предоставить информацию о потреблении на 1 января.
data_consumption_2o_solar_weeklyShift = data_consumption_2o_solar.copy() data_consumption_2o_solar_weeklyShift['Last_Week'] = data_consumption_2o_solar['Consumption'].shift(7) data_consumption_2o_solar_weeklyShift = data_consumption_2o_solar_weeklyShift.dropna()
Сброс Train / Test + GridSearch + Проверка работоспособности
X_train_2o_solar_weeklyShift = data_consumption_2o_solar_weeklyShift[:'2016'].drop(['Consumption'], axis = 1) y_train_2o_solar_weeklyShift = data_consumption_2o_solar_weeklyShift.loc[:'2016', 'Consumption'] X_test = data_consumption_2o_solar_weeklyShift['2017'].drop(['Consumption'], axis = 1) y_test = data_consumption_2o_solar_weeklyShift.loc['2017', 'Consumption'] model = RandomForestRegressor() param_search = { 'n_estimators': [20, 50, 100], 'max_features': ['auto', 'sqrt', 'log2'], 'max_depth' : [i for i in range(5,15)] } tscv = TimeSeriesSplit(n_splits=10) gsearch = GridSearchCV(estimator=model, cv=tscv, param_grid=param_search, scoring = rmse_score) gsearch.fit(X_train_2o_solar_weeklyShift, y_train_2o_solar_weeklyShift) best_score = gsearch.best_score_ best_model = gsearch.best_estimator_ y_true = y_test.values y_pred = best_model.predict(X_test) regression_results(y_true, y_pred)
И мы сделали это снова .. Ошибки были уменьшены, а r-квадрат увеличился. Мы могли бы продолжать добавлять более актуальные функции, но я думаю, вы уже поняли идею!
График важности функции
Как мы правильно предположили, значение (t-7)
-го дня является гораздо более сильным предиктором, чем значение (t-1)
-го дня.
Заключение
В этой статье мы узнали, как моделировать данные временных рядов, проводить перекрестную проверку данных временных рядов и точно настраивать гиперпараметры нашей модели. Нам также удалось снизить RMSE с 85,61 до 54,57 для прогнозирования энергопотребления.
В Части 3 этой серии мы будем работать над тематическим исследованием, анализирующим данные временных рядов, генерируемых центрами обработки вызовов, в основном работая над анализом (пугающего) увеличения показателей отказа. Будьте на связи…
До скорого :)