Анализ и прогноз цен на рынке жилья с использованием перекрестной проверки и поиска по сетке в нескольких регрессионных моделях.
В этой статье я анализирую факторы, связанные с ценами на жилье в Мельбурне, и делаю прогнозы цен на жилье, используя несколько методов машинного обучения: Линейная регрессия, Ридж-регрессия, K-ближайших соседей (далее KNN) и Дерево решений. Используя методы перекрестной проверки и поиска по сетке, я нахожу оптимальные значения гиперпараметров в каждой модели и сравниваю результаты, чтобы найти лучшую модель машинного обучения для прогнозирования цен на жилье в Мельбурне.
Весь код этого проекта и данные находятся здесь.
Набор данных
Данные для этого анализа — Рынок жилья Мельбурна из набора данных Kaggle. Общее количество строк и столбцов составляет 34 857 и 21 соответственно. Столбцы следующие:
df = pd.read_csv('...\Melbourne_housing_FULL.csv') df.columns.to_list() ['Suburb','Address','Rooms','Type','Method','SellerG','Date','Distance','Postcode','Bedroom2','Bathroom','Car','Landsize','BuildingArea','YearBuilt','CouncilArea','Latitude','Longitude','Regionname','Propertycount','Price']
Предварительная обработка данных
В этом разделе кратко объясняется, как обрабатываются пропущенные значения и выбросы.
Отсутствующие значения
Используя библиотеку missingno
в Python, проверьте отсутствующие значения в наборе данных.
import missingno as msno msno.bar(df)
- Согласно коэффициентам корреляции,
Rooms
является хорошим показателем дляBathroom
иCar
. После создания категориального признака, указывающего, является ли уровеньRooms
высоким, средним или низким для каждого дома, медианыBathroom
иCar
рассчитывается в группе с одинаковым уровнемRooms
. Затем вычисленные медианы помещаются для отсутствующих значений вBathroom
иCar
. - Строки с отсутствующими значениями в
Price
удаляются. - Остальные функции, имеющие пропущенные значения, отбрасываются.
Выбросы
Для выбросов я использую метод Межквартильный диапазон (IQR). Этот метод находит точки данных, которые выходят за пределы 1,5-кратного межквартильного диапазона выше 3-го квартиля (Q3) и ниже 1-го квартиля (Q1), и исключает эти записи из анализа.
Q1 и Q3 для каждого числового признака и цели Price
следующие:
Price
— 635 000 долларов и 1 295 000 долларовRooms
— 2-х комнатная и 4-х комнатнаяDistance
— 6,4 км и 14 кмBathroom
— 1 и 2 комн.Car
— 1 и 2 места
Для Price
точка данных для 1,5-кратного IQR выше Q3 (верхний ус) составляет 2 285 000 долларов, а точка данных для 1,5-кратного IQR ниже Q1 (нижний ус) составляет -355 000 долларов. Распределение цены после удаления выбросов выглядит следующим образом:
import seaborn as sns import matplotlib.pyplot as plt Q1 = df['Price'].quantile(0.25) Q3 = df['Price'].quantile(0.75) IQR = Q3-Q1 Lower_Whisker = Q1 - 1.5*IQR Upper_Whisker = Q3 + 1.5*IQR df = df[(df['Price']>Lower_Whisker)&(df['Price']<Upper_Whisker)] plt.figure(figsize=(10,5)) sns.distplot(df['Price'],hist=True, kde=False, color='blue') plt.ylabel('Counts')
Исследовательский анализ данных (EDA)
Числовые характеристики
В нашем анализе есть четыре числовых признака: Rooms
, Bathroom
, Car
и Distance
. Лучший способ сразу увидеть взаимосвязь между числовыми характеристиками и целью — нарисовать диаграммы рассеяния. График hexbin разбивает области на несколько hexbin на графике, а цвет каждого hexbin обозначает количество точек данных. Чем темнее цвет шестнадцатеричной ячейки, тем больше точек данных в шестнадцатеричной области.
Давайте посмотрим на диаграммы разброса hexbin, чтобы показать взаимосвязь числовых признаков с Price
.
import matplotlib.image as mpimg JG1 = sns.jointplot('Rooms', 'Price', data=df, kind='hex', color='g') JG2 = sns.jointplot('Bathroom', 'Price', data=df, kind='hex', color='b') JG3 = sns.jointplot('Car', 'Price', data=df, kind='hex', color='r') JG4 = sns.jointplot('Distance', 'Price', data=df, kind='hex', color='orange') JG1.savefig('JG1.png') plt.close(JG1.fig) JG2.savefig('JG2.png') plt.close(JG2.fig) JG3.savefig('JG3.png') plt.close(JG3.fig) JG4.savefig('JG4.png') plt.close(JG4.fig) f, ax = plt.subplots(2,2,figsize=(20,16)) ax[0,0].imshow(mpimg.imread('JG1.png')) ax[0,1].imshow(mpimg.imread('JG2.png')) ax[1,0].imshow(mpimg.imread('JG3.png')) ax[1,1].imshow(mpimg.imread('JG4.png')) [ax.set_axis_off() for ax in ax.ravel()] plt.tight_layout()
Графики выше ясно показывают положительные отношения Rooms
, Bathroom
, Car
с Price
и отрицательные отношения Distance
с Price
.
Категориальные характеристики
Категориальные признаки: Regionname
и Type
.
Название региона
Regionname
имеет 7 уникальных значений: Северный митрополит, Южный митрополит, Западный митрополит, Восточный митрополит, Юго-восточный митрополит, Северная Виктория, и восточная восточная Виктория. Эти названия регионов — Избирательные округа штата Виктория. В основном эти регионы делятся на восемь областей. В наших данных нет данных о жилье в Западной Виктории.
Давайте посмотрим на диаграмму между Regionname
и Price
.
plt.figure(figsize=(12,6)) sns.boxplot('Regionname', 'Price', data=df, width=0.3, palette="Set2") plt.xticks(rotation=45) df['Regionname'].value_counts()
Чтобы использовать эту функцию в анализе, я создаю манекены для этой функции и объединяю их в набор данных.
regionname = pd.get_dummies(df['Regionname'],drop_first=True) df = pd.merge(df, regionname, left_index=True, right_index=True) df.drop('Regionname', axis=1, inplace=True)
Тип
Наш набор данных делит типы жилья на три категории:
h
— дом, коттедж, вилла, полуторка и террасаu
— блок и дуплексt
— таунхаус
plt.figure(figsize=(10,5)) sns.boxplot('Type', 'Price', data=df, width=0.3, palette="Set2") df['Type'].value_counts()
Около 60 % наблюдений относятся к типу h
, а около 25 % — к типу u
. На блочной диаграмме тип h
имеет наибольшую дисперсию. Самые дорогие и самые дешевые дома относятся к типу h
.
Давайте также создадим дамми для Type
и объединим их в набор данных.
house_type = pd.get_dummies(df['Type'], drop_first=True) df = pd.merge(df,house_type, left_index=True, right_index=True) df.drop('Type', axis=1, inplace=True)
Прогнозное моделирование
В этом анализе используются следующие модели регрессии: Линейная регрессия, Ридж-регрессия, K- Ближайшие Соседи и Дерево решений.
Основные прогнозы
Чтобы проверить эффективность прогнозов для каждой регрессионной модели, я сначала разделил данные на обучающую и тестовую выборки. Сначала я подгоняю модели, используя обучающую выборку, а затем предсказываю цены на жилье, используя проверочную выборку. Данные тестирования составляют 30% от всех данных.
from sklearn.model_selection import train_test_split from sklearn import metrics from sklearn.model_selection import cross_validate X=df.drop('Price', axis=1) y=df['Price'] train_X, test_X, train_y, test_y = train_test_split(X,y,test_size=0.3, random_state=0)
Чтобы измерить эффективность прогнозов для каждой модели, я использую две метрики производительности: R² (Коэффициент детерминации) и MSE (Среднеквадратическая ошибка).
- R²
Первый — это коэффициент детерминации, который обычно выражается как R². Коэффициент детерминации – это отношение дисперсии цели, объясненной или предсказанной моделью, к общей дисперсии цели. Он находится в диапазоне от 0 до 1, и чем ближе значение к 1, тем лучше модель объясняет или предсказывает дисперсию цели.
- MSE
Второй показатель — это Среднеквадратическая ошибка (MSE). MSE — это среднее значение квадрата разницы между оценочными или прогнозируемыми значениями и фактическими значениями цели. Это всегда больше нуля. Более низкое значение MSE указывает на более высокую точность прогнозов модели. В этом анализе я использую квадратный корень из этой метрики (RMSE).
Для удобства создадим функцию для расчета R² и RMSE, а также функцию для сравнения распределений фактических значений и прогнозируемых значений для каждой модели.
def Predictive_Model(estimator): estimator.fit(train_X, train_y) prediction = estimator.predict(test_X) print('R_squared:', metrics.r2_score(test_y, prediction)) print('Square Root of MSE:',np.sqrt(metrics.mean_squared_error(test_y, prediction))) plt.figure(figsize=(10,5)) sns.distplot(test_y, hist=True, kde=False) sns.distplot(prediction, hist=True, kde=False) plt.legend(labels=['Actual Values of Price', 'Predicted Values of Price']) plt.xlim(0,) def FeatureBar(model_Features, Title, yLabel): plt.figure(figsize=(10,5)) plt.bar(df.columns[df.columns!='Price'].values, model_Features) plt.xticks(rotation=45) plt.title(Title) plt.ylabel(yLabel)
Линейная регрессия
from sklearn.linear_model import LinearRegression lr = LinearRegression() Predictive_Model(lr)
R² линейной регрессии составляет 0,6146, что означает, что модель может предсказать около 60% дисперсии цен на жилье в данных. RMSE составляет 264 465. Это означает, что для всех прогнозов для набора тестов средняя разница для каждого прогноза составляет 264 465 долларов.
Регрессия хребта
from sklearn.linear_model import Ridge rr = Ridge(alpha=100) Predictive_Model(rr)
Приведенный выше результат получен с параметром регуляризации (alpha
), равным 100. R² этой модели составляет 0,6133, а RMSE — 264 920.
К ближайших соседей (KNN)
from sklearn.neighbors import KNeighborsRegressor knn = KNeighborsRegressor(n_neighbors=5) Predictive_Model(knn)
Приведенный выше результат получен с числом соседей, равным 5. R² этой модели составляет 0,7053, а RMSE — 231 250.
Дерево решений
from sklearn.tree import DecisionTreeRegressor dt = DecisionTreeRegressor(max_depth=15, random_state=0) Predictive_Model(dt)
В модели дерева решений max depth
является одним из факторов, предотвращающих проблему переобучения модели. Чем больше глубина дерева, тем больше у дерева ветвей и оно становится больше. Поскольку дерево имеет больше ветвей, прогноз для обучающей выборки может быть более точным. Однако существует большая дисперсия в прогнозировании тестового набора. Таким образом, оптимальная настройка max depth
важна, чтобы избежать проблемы перенастройки. В приведенном выше примере max depth
установлено на 15. R² этой модели составляет 0,6920, а RMSE — 236 424.
Сводка по эффективности
regressor = ['Linear Regression', 'Ridge Regression', 'KNN', 'Decision Tree'] models = [LinearRegression(), Ridge(alpha=100), KNeighborsRegressor(n_neighbors=5), DecisionTreeRegressor(max_depth=15, random_state=0)] R_squared = [] RMSE = [] for m in models: m.fit(train_X, train_y) prediction_m = m.predict(test_X) r2 = metrics.r2_score(test_y, prediction_m) rmse = np.sqrt(metrics.mean_squared_error(test_y, prediction_m)) R_squared.append(r2) RMSE.append(rmse) basic_result = pd.DataFrame({'R squared':R_squared,'RMSE':RMSE}, index=regressor) basic_result
В приведенной выше таблице KNN кажется оптимальной моделью для прогнозирования цен на жилье в Мельбурне. Однако пока рано делать выводы, так как есть еще много вещей, которые нужно учитывать. Во-первых, мы используем только один конкретный набор обучающих и тестовых наборов, а во-вторых, для каждой модели мы выбираем одно конкретное значение для каждого гиперпараметра. Чтобы получить надежный результат, охватывающий эти проблемы, нам также необходимо пройти процесс перекрестной проверки и поиска по сетке.
Перекрестная проверка и поиск по сетке
Перекрестная проверка (CV) — процедура повторной выборки, когда количество данных ограничено. Это случайным образом разбивает все данные на K-сгибы, подбирает модель с использованием (K-1) сгибов, проверяет модель с использованием оставшегося сгиба, а затем оценивает производительность с помощью метрик. После этого CV повторяет весь этот процесс до тех пор, пока каждая K-кратность не будет использоваться в качестве тестового набора. Среднее значение K-числа оценок метрики является окончательной оценкой производительности модели.
Поиск по сетке — это процесс настройки гиперпараметров для поиска оптимальных значений параметров модели. Результаты прогнозирования могут различаться в зависимости от конкретных значений параметров. Метод поиска по сетке применяет все возможные кандидаты в параметры, чтобы найти оптимальный, чтобы дать наилучшие прогнозы для модели.
Линейная регрессия
scoring={'R_squared':'r2','MSE':'neg_mean_squared_error'} def CrossVal(estimator): scores = cross_validate(estimator, X, y, cv=10, scoring=scoring) r2 = scores['test_R_squared'].mean() mse = abs(scores['test_Square Root of MSE'].mean()) print('R_squared:', r2) print('Square Root of MSE:', np.sqrt(mse)) CrossVal(LinearRegression()) R_squared: 0.5918115585795747 Square Root of MSE: 269131.0885647736
Поскольку в нашем анализе линейная регрессия не имеет гиперпараметров, здесь выполняется только CV. Количество складок в CV установлено равным 10. Среднее значение R² составляет 0,5918, а RMSE — 269131.
Регрессия хребта
Параметр регуляризации в гребневой регрессии выражается как alpha
в sklearn. Поскольку GridSearchCV
в sklearn включает процесс перекрестной проверки, процесс выполнения cross_validate
опущен. Набор сетки для alpha
здесь установлен как [0.01, 0.1, 1, 10, 100, 1000, 10000]
.
from sklearn.model_selection import GridSearchCV def GridSearch(estimator, Features, Target, param_grid): for key, value in scoring.items(): grid = GridSearchCV(estimator, param_grid, cv=10, scoring=value) grid.fit(Features,Target) print(key) print('The Best Parameter:', grid.best_params_) if grid.best_score_ > 0: print('The Score:', grid.best_score_) else: print('The Score:', np.sqrt(abs(grid.best_score_))) print() param_grid = {'alpha':[0.01, 0.1, 1, 10, 100, 1000, 10000]} GridSearch(Ridge(), X, y, param_grid) R_squared The Best Parameter: {'alpha': 10} The Score: 0.5918404945235951 Square Root of MSE The Best Parameter: {'alpha': 10} The Score: 269125.20208461734
Результат показывает, что наилучшее значение alpha
равно 10. R² и RMSE составляют 0,5918 и 269125 соответственно при alpha = 10
.
K-ближайшие соседи
Гиперпараметр для KNN, который мы используем в этом анализе, — это количество ближайших соседей (n_neighbors
). Диапазон для сетки — целые числа от 5 до 25.
param_grid = dict(n_neighbors=np.arange(5,26)) GridSearch(KNeighborsRegressor(), X, y, param_grid) R_squared The Best Parameter: {'n_neighbors': 16} The Score: 0.6973921821195777 Square Root of MSE The Best Parameter: {'n_neighbors': 16} The Score: 232900.0204190322
Оптимальное число n_neighbors
— 16. R² — 0,6974, а RMSE — 232900. Мы можем видеть, что 16 является оптимальным значением для n_neighbors
в нашем анализе, взглянув на кривую проверки.
from sklearn.model_selection import validation_curve def ValidationCurve(estimator, Features, Target, param_name, Name_of_HyperParameter, param_range): train_score, test_score = validation_curve(estimator, Features, Target, param_name, param_range,cv=10,scoring='r2') Rsqaured_train = train_score.mean(axis=1) Rsquared_test= test_score.mean(axis=1) plt.figure(figsize=(10,5)) plt.plot(param_range, Rsqaured_train, color='r', linestyle='-', marker='o', label='Training Set') plt.plot(param_range, Rsquared_test, color='b', linestyle='-', marker='x', label='Testing Set') plt.legend(labels=['Training Set', 'Testing Set']) plt.xlabel(Name_of_HyperParameter) plt.ylabel('R_squared') ValidationCurve(KNeighborsRegressor(), X, y, 'n_neighbors', 'K-Neighbors',np.arange(5,26))
Дерево решений
В модели дерева решений может учитываться несколько гиперпараметров. В нашем анализе только max_depth
является вариантом гиперпараметра. Диапазон max_depth
для проверки — это целые числа от 2 до 14.
param_grid=dict(max_depth=np.arange(2,15)) GridSearch(DecisionTreeRegressor(), X, y, param_grid) R_squared The Best Parameter: {'max_depth': 9} The Score: 0.6844562874572124 Square Root of MSE The Best Parameter: {'max_depth': 9} The Score: 237708.76352194021
Результат показывает, что оптимальное значение для max_depth
равно 9. R² составляет 0,6845, а RMSE составляет 237708 при max_depth=9
. Это подтверждается и на кривой проверки.
ValidationCurve(DecisionTreeRegressor(), X, y, 'max_depth', 'Maximum Depth', np.arange(4,15))
Сводка перекрестной проверки
В таблице и на графиках ниже показаны баллы R² для каждого раунда тестирования в CV. Поскольку cv
установлено равным 10, у нас есть 10 раундов тестирования.
lr_scores = cross_validate(LinearRegression(), X, y, cv=10, scoring='r2') rr_scores = cross_validate(Ridge(alpha=10), X, y, cv=10, scoring='r2') knn_scores = cross_validate(KNeighborsRegressor(n_neighbors=16), X, y, cv=10, scoring='r2') dt_scores = cross_validate(DecisionTreeRegressor(max_depth=9, random_state=0), X, y, cv=10, scoring='r2') lr_test_score = lr_scores.get('test_score') rr_test_score = rr_scores.get('test_score') knn_test_score = knn_scores.get('test_score') dt_test_score = dt_scores.get('test_score') box= pd.DataFrame({'Linear Regression':lr_test_score, 'Ridge Regression':rr_test_score, 'K-Nearest Neighbors':knn_test_score, 'Decision Tree':dt_test_score}) box.index = box.index + 1 box.loc['Mean'] = box.mean() box
Согласно результату в таблице, лучшей моделью машинного обучения в нашем анализе является KNN, поскольку среднее значение баллов для каждого раунда является самым высоким для KNN.
f,ax=plt.subplots(1,2, figsize=(12,5)) sns.boxplot(data=box.drop(box.tail(1).index), width=0.3, palette="Set2", ax=ax[0]) ax[0].set_ylabel('R squared') sns.lineplot(data=box.drop(box.tail(1).index), palette="Set2", ax=ax[1]) ax[1].set_xticks(np.arange(1,11,1)) ax[1].set_xlabel('K-th Fold')
На приведенных выше графиках и линиях показаны распределения и изменения оценок для каждой модели. Модель дерева решений, а также KNN показывают хорошие результаты в нашем анализе. Линейная и гребенчатая регрессии не показывают существенной разницы в их производительности.