Привет! В этой статье я хотел бы представить вам, как я создал свой собственный набор данных о жилье и обучил модель для прогнозирования цен на аренду. В моем проекте используются такие технологии, как Python, sklearn и Beautiful Soup. Я также пробовал различные алгоритмы машинного обучения, чтобы увидеть, какой из них будет наиболее эффективным. Добро пожаловать снова и давайте начнем наше путешествие :)

PS. Вы можете увидеть код в моем репозитории GitHub для лучшего понимания.

Получение данных (веб-скрейпинг)

Для создания собственного набора данных мне пришлось найти сайт с арендованными домами в одном из городов. Я нацелился на Варшаву, так как это самый большой город в Польше, поэтому потенциально там должно быть самое большое количество предложений. Я нашел один веб-сайт, на котором показаны дома, сдаваемые в аренду, и проверил, разрешает ли этот веб-сайт их сдавать.

После этого я определил, какие функции мне нужны в моем наборе данных, и предположил, что будет 4 столбца:

  • район города
  • номера
  • квадратные метры
  • цена

Я собираюсь обучить модель прогнозировать цену аренды дома на основе района, в котором находится дом, количества комнат и квадратных метров.

Набор данных

Я получил 1038 строк с 4 функциями, которые я только что упомянул. Валюта, естественно, PLN (польский злотый). Один столбец является категориальным, поэтому мне нужно будет использовать OrdinalEncoder для кодирования значений «район», чтобы они были числами. Давайте кратко рассмотрим описание фрейма данных.

Единственное, на что стоит обратить внимание, это то, что 75% данных имеют цену 5000 злотых и ниже, когда максимум составляет 250 000 злотых. Это может означать, что в наборе данных есть сильные выбросы или некоторые дома не сдаются в аренду, а продаются.

Визуализация данных и очистка данных

Мы видим, что район «Беляны» кажется самым дорогим районом, прямо перед «Centrum» и «Śródmieście», которые в основном являются центральными районами. города. Эта информация, однако, должна также показать нам, сколько домов мы включаем в каждый район, чтобы подсчитать среднюю цену.

У меня была идея показать эти особенности на диаграмме рассеяния.

Так что на этот раз размер пузыря означает среднюю цену. Чем он больше, тем больше нам придется платить за аренду. Ось Y показывает, сколько предложений домов доступно в том или ином районе. Наконец, по оси X показаны названия районов.

Видно, что некоторые пузыри расположены довольно низко на графике, поэтому их среднее значение цены не включало много предложений.

Далее я собираюсь использовать seaborn для отображения статистики некоторых функций и проверки их линейности. Позвольте мне показать вам анализ столбца «rooms» в качестве примера.

Мы видим, что цены немного растут при большем количестве номеров. Наиболее распространены дома с 2 и 3 комнатами. Этот дистрибутив кажется вполне приемлемым, поэтому я пока оставлю его как есть.

Здесь отчетливо видны некоторые выбросы. Я собираюсь удалить столбцы, где значение square_meters больше 200. Я также удалю столбцы, где цена превышает 30 000 злотых. Я потеряю некоторые данные, но, надеюсь, улучшу точность модели благодаря удалению выбросов.

Это выглядит немного лучше. Кроме того, гистограмма справа выглядит более полной. Вы можете выполнить дополнительные шаги по визуализации данных и очистке данных в зависимости от ваших потребностей. Я собираюсь остановиться здесь и сделать шаг вперед, чтобы подготовить свои данные для обучения.

Подготовка к обучению

Кодирование категориальных значений

Наша модель будет работать с числами, поэтому нам нужно сопоставить названия районов с плавающими или целыми числами. Для этого я использовал OrdinalEncoder.

Метод fit() обнаружил уникальные значения в столбце «район», а метод transform() изменил эти значения на числа, относящиеся к конкретным именам. Таким образом, когда мы попытаемся преобразовать имя «Bemowo», мы всегда получим значение 0,0 и т. д.

Разделите набор данных на наборы для обучения и тестирования.

Сначала я отделил столбцы характеристик (комнаты, район, квадратные_метры) от целевого столбца (цена). Затем я использовал функцию train_test_split() для создания наборов данных для поездов и тестов. test_size=0.2означает, что тестовый набор будет содержать 20 % набора данных.

Стандартизация данных

Я использовал StandardScaler для стандартизации данных, чтобы моя модель линейной регрессии работала лучше.

Обучение

Древо решений

Я начал с DecisionTreeRegressor в качестве первого алгоритма. На примере графика это выглядит очень хорошо, поскольку прогнозы модели в основном охватывают цены на жилье. Обратите внимание, что деревья решений склонны к переоснащению, поэтому мы должны стараться содержать набор данных в чистоте от шума, а дерево не должно быть слишком глубоким. Посмотрим на показатели производительности:

### Checking how does the model perform on the test data ###

R2: 0.7095148024406142
MAE: 805.3158097128395
MSE: 3989621.41933594
RMSE: 1997.4036696010999

Это означает, что точность достигла уровня 70% и в среднем ошибка прогноза должна быть в пределах 805 злотых от реальной цены. Это зависит от наших ожиданий относительно приемлемости этой разницы. На мой взгляд, для целей оценки эти значения приемлемы.

Случайный лес

Случайный лес — это своего рода расширение дерева решений, поскольку оно состоит из нескольких деревьев решений и учитывает результаты каждого отдельного дерева. Это простая реализация без какой-либо настройки гиперпараметров, и на первый взгляд кажется, что производительность аналогична дереву решений, реализованному ранее. Давайте посмотрим, как это работает в целом:

### Checking how does the model perform on the test data ###

R2: 0.8296758898335707
MAE: 786.1530254977203
MSE: 2339288.62420055
RMSE: 1529.47331594917

Точность намного выше: она достигла 82,9%. Средняя абсолютная ошибка составляет 786, что означает, что большинство прогнозов должны отличаться от реальной цены до 786 злотых. Это на самом деле удовлетворительный результат.

Линейная регрессия

Линейная регрессия кажется менее точной, чем два предыдущих алгоритма для нашего набора данных. Давайте вспомним, что линейная регрессия предполагает линейную зависимость между функциями, и если есть какие-то нелинейные отношения, этот алгоритм может с трудом зафиксировать эти отношения.

### Checking how does the model perform on the test data ###

R2: 0.5882496009630866
MAE: 1251.2523622833514
MSE: 5655118.488720776
RMSE: 2378.049303256931

Уровень точности составляет 58%, что намного меньше по сравнению с деревом решений или случайным лесом. Стоит отметить, что алгоритм линейной регрессии гораздо более чувствителен к выбросам в наборе данных. Нам пришлось бы очищать данные с гораздо большей точностью, но в то же время мы могли бы потерять некоторые данные о конкретных домах.

Давайте визуализируем отношения между функциями и ценой.

На тепловой карте видно, что квадратные_метры и комнаты связаны с ценой, но район не так сильно коррелирует. Похоже, что это значение вызывает нелинейность набора данных, что делает линейную регрессию менее эффективной.

Бонус: API цен на дома в Варшаве

Итак, я обучил модель, и как мне теперь сделать ее полезной? Давайте сделаем быстрый API, который позволит пользователям отправлять к нему свои запросы, содержащие название района Варшавы, количество комнат и квадратных метров дома. В ответ API вернет прогнозируемую месячную арендную плату за дом.

Что-то быстрое. FastAPI :)

Я использовал FastAPI для создания конечной точки, описанной выше. Пользователи должны отправить запрос POST с данными дома в теле запроса. С FastAPI будет легко тестировать благодаря интерактивной документации. Взглянем:

# api.py module

from fastapi import FastAPI
from models.housing_model import HouseFeatures, PredictedPrice
from regression.random_forest_utils import RandomForestModelUtils

app = FastAPI()


@app.post("/predict")
def predict(features: HouseFeatures) -> PredictedPrice:
    """ Handle users' requests and return estimated price. """
    rfmu = RandomForestModelUtils()
    price = rfmu.predict(features.district, features.rooms, features.square_meters)
    return PredictedPrice(price=price)

В этом модуле RandomForestModelUtils — это пользовательский класс, который я создал для загрузки модели, преобразования входящих данных в понятные модели и возврата прогнозируемой цены.

# housing_model.py module
from pydantic import BaseModel, validator

DISTRICTS = ['Bemowo', 'Białołęka', 'Bielany', 'Centrum', 'Mokotów', 'Ochota',
            'PragaPołudnie', 'PragaPółnoc', 'Rembertów', 'Targówek', 'Ursus',
            'Ursynów', 'Wawer', 'Wesoła', 'Wilanów', 'Wola', 'Włochy',
            'Śródmieście', 'Żoliborz']

class HouseFeatures(BaseModel):
    district: str
    square_meters: float
    rooms: int
    
    @validator('district')
    def name_in_districts(cls, v):
        if v not in DISTRICTS:
            raise ValueError(f'{v} must be one of: {DISTRICTS}')
        return v

class PredictedPrice(BaseModel):
    price: float
    
    @validator('price')
    def two_digit_numbers(cls, v):
        return round(v,2)

В модуле Housing_Model я определил все доступные районы. HouseFeatures имеет один валидатор, который проверяет, входит ли входящее название района в число известных модели.

# random_forest_utils.py module
import joblib
import numpy as np
from sklearn.preprocessing import OrdinalEncoder
from models.housing_model import DISTRICTS

class RandomForestModelUtils:
    def __init__(self):
        self.encoder = OrdinalEncoder()
        district_arrays = [[d] for d in DISTRICTS]
        self.encoder.fit(district_arrays)

    def load_model(self):
        self.model = joblib.load("./regression/random_forest_houses.joblib")
    
    def predict(self, district: str, rooms: int, square_meters: float):
        # Load modal
        self.load_model()
        
        # Get district converted to number
        district = self.encoder.transform([[district]])[0][0]
        
        # Convert this data to numpy array
        features_np_array = np.array([district, rooms, square_meters])
        
        # Predict
        result = self.model.predict([features_np_array])[0]
        
        return result

RandomForestModelUtils содержит всю логику сопоставления данных и возврата предсказанного значения. Я использовал joblib, чтобы сохранить обученную модель в файл, а затем загрузить ее в память программы.

ДЕМО

Давайте поищем предложения по аренде дома в Варшаве и посмотрим, сможет ли модель оценить цену.

Итак, вот одно предложение со следующими данными для нашей модели:

  • Район: Бемово
  • Комнаты: 1
  • Квадратные метры: 30
  • Цена: 2600 злотых

Наша модель оценила цену в 2524 злотых, тогда как реальная стоимость составляет 2600 злотых! Это очень близкий прогноз :)

Краткое содержание

Вот и все! Я показал вам шаги, которые я предпринял, чтобы обучить модель машинного обучения для прогнозирования цен на жилье в Варшаве. Сначала я создал свой собственный набор данных, удалив одну из страниц с предложениями домов. Затем я визуализировал данные и проанализировал их. Данные были подготовлены для обучения, и я сравнил алгоритмы дерева решений, случайного леса и линейной регрессии. В итоге алгоритм Random Forest оказался лучшим выбором с точностью более 80%. Я также кратко показал вам, как использовать вашу модель в конечной точке FastAPI.

Возможно, можно было бы повысить точность модели, используя какой-то другой алгоритм или нейронные сети. Кроме того, набор данных может быть расширен за счет большего количества домов, чтобы было больше данных для изучения.

Я также выбрал только 3 функции для модели, чтобы учиться, однако на веб-сайте, который я удалил, было больше данных, таких как доступ к саду, доступ к гаражу, год постройки дома и т. д. Я мог настроить свой блокировщик, чтобы получить все данные. эти данные, чтобы сделать прогнозы более точными.

По сути, это бесконечные открытия с обработкой данных :)

Спасибо за чтение!

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.