Анализ и прогноз цен на рынке жилья с использованием перекрестной проверки и поиска по сетке в нескольких регрессионных моделях.

В этой статье я анализирую факторы, связанные с ценами на жилье в Мельбурне, и делаю прогнозы цен на жилье, используя несколько методов машинного обучения: Линейная регрессия, Ридж-регрессия, 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². Коэффициент детерминации – это отношение дисперсии цели, объясненной или предсказанной моделью, к общей дисперсии цели. Он находится в диапазоне от 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 показывают хорошие результаты в нашем анализе. Линейная и гребенчатая регрессии не показывают существенной разницы в их производительности.