Machine Learning и оценка недвижимости

Max Bobkov
13 min readFeb 2, 2020

--

Как писал Вастрик в своей шикарной обзорной статье:

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

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

Давайте разберемся, как это работает на примере вторичного рынка квартир. Да, рынок специфический, половина объявлений — фейки и нелепые фантазии продавцов, не готовых к сделке. Но это данные, на которых можно понять принципы и идти дальше.

Программисты в качестве примитивного упражнения пишут код, печатающий “Hello World!”, а мы будем строить модель, предсказывающую цены квартир в Москве. Окей, предсказывающую цены предложений по параметрам жилья, такая формулировка честнее.

О чем вообще речь?

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

Частые задачи для таких алгоритмов:

  • классификация —например, разделение грибов по классам съедобный/ядовитый на основании цвета, формы шляпки, наличия юбки и т.п.;
  • регрессия — как раз наша тема, вычисление цены предложения квартиры, исходя из площади, местоположения, местоположения и местоположения));
  • кластеризация, это когда алгоритм растаскивает данные по группам, самостоятельно находя признаки, по которым их можно сгруппировать;
  • поиск аномалий — к примеру, снимая телеметрию со станка, можно засечь проблему до того, как условный шпиндель выйдет из строя. При этом программа сама сообразит, что поток данных стал отличаться от обычного;
  • всякие сложные штуки вроде компьютерного зрения — когда машина отделяет на изображениях котиков от собачек. Это больше всего тянет на магию, но важно помнить, что для алгоритма файл с фотографией это тоже цифры — значения яркости и цвета пикселей по всей площади картинки. От этого можно плясать дальше, находя типичные для котиков сочетания линий, острых углов ушей и все в таком духе.

Большая часть математики, на которой это работает, была написана лет 60–70 назад, но сейчас тема выстрелила по двум основным причинам: бизнес стал накапливать данные и у нас появились мощные компы (и видеокарты, на которых можно распараллелить вычисления, чтобы не ждать неделями, пока обучится модель).

Для погружения в тему советую сайт kaggle.com, на котором проходят соревнования по ML. Там команды бьются за сотые доли процента качества моделей, чтобы забрать денежный приз от организатора, задающего тему исследования и публикующего свои реальные данные.

К примеру, три года назад Сбербанк устраивал конкурс по предсказанию цен на ипотечные квартиры: https://www.kaggle.com/c/sberbank-russian-housing-market, больше 3 000 команд боролись за приз в 25 000 долларов. А здесь победитель целый час рассказывает, как шел к успеху: https://www.youtube.com/watch?v=Eo4WMlcT7uo.

https://xkcd.ru/605/

Единственное, надо помнить, что ML в реальных проектах отличается от соревнований примерно так же, как конвейер для серийных Фольксвагенов от гаража Формулы-1, настраивающего болид для конкретной трассы. Зато на Кэггле подробно разбирают решения и публикуют примеры кода, полезно посмотреть как задачу решали более умные ребята.

Матчасть

Сразу оговорюсь, в статье речь только про “обучение с учителем”. Это ситуация, когда алгоритм смотрит на данные, заранее размеченные человеком. В них указано — квартира с такими-то параметрами выставлена на Циане за 7 млн, а с такими-то и на другой улице за 7 с половиной. Чем больше примеров, тем лучше и точнее обучится алгоритм.

Чтобы оценить качество предсказаний модели, данные (датасет) случайным образом делят на обучающую и тестовую выборки. Обычно в соотношении 70/30, чтобы сохранить часть примеров в тайне от алгоритма. Модель учится на первой выборке, сопоставляя параметры квартир и их цены. Затем скармливаем ей характеристики квартир из тестовой выборки (но цены из тестовой не показываем, она попытается угадать их сама). Последним действием вычисляем ошибку, грубо говоря, насколько предсказанная стоимость отклонилась от фактической.

Метрики качества нужны, чтобы сравнивать модели между собой и понимать, насколько хорошо модель описывает реальность. Для своих исследований я использовал две метрики:

  • Средняя абсолютная процентная ошибка (MAPE)— показывает среднее арифметическое значение всех абсолютных (взятых по модулю) процентных ошибок прогноза (Ф — фактическое значение стоимости, П — прогнозное значение стоимости). Рассчитывается по формуле:
  • Медианная абсолютная процентная ошибка (MedAPE)— та же идея вычисления абсолютной процентной ошибки, но вместо среднего арифметического берем более устойчивую к выбросам медиану. Именно своей MedAPE в районе 2% хвастается сайт https://www.zillow.com/, построивший одну из самых точных моделей оценки домов в США.

Алгоритмы

Дерево решений (Decision tree) — один из самых известных алгоритмов машинного обучения, хорошо работающий с табличными данными. Его основная идея — последовательно разбивать (сплитить) выборку на все более мелкие части так, чтобы в конце максимально точно разложить примеры по правильным кучкам. Украду у Вастрика хорошую картинку, потому что лучше один раз увидеть:

https://vas3k.ru/blog/machine_learning/

Картинка про классификацию, но не суть. На каждом узле принятия решений есть некий критерий и два варианта ответа — да или нет. Критерии разбиения могут не всегда выглядеть адекватно для человека, например “Пробег автомобиля больше 56 452 км?”, но можно быть уверенным, что алгоритм на каждом шаге разбивал набор данных максимально эффективно, начиная с самых важных критериев. В итоге пример новых данных можно провести через готовое дерево решений, следуя по нужным развилкам, и получить в конце результат (в нашем случае предсказание цены).

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

Оказалось, что недостатки деревьев можно обернуть во благо, если собрать из них, так называемый ансамбль, Случайный лес (Random forest). Идея случайного леса в том, что строится сразу много деревьев решений, слабых по отдельности, но сильных своим общим вердиктом (при регрессии предсказанные значения деревьев усредняются, при классификации финальное решение выбирается голосованием). При этом, каждое из слабых деревьев строится только на части датасета. Изобретение случайного леса позволило здорово улучшить качество предсказаний, настолько, что одиночные деревья сейчас фактически не используются.

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

Последние годы на Кэггле почти всегда лидируют решения, построенные на алгоритмах бустинга XGBoost и LightGBM. Кстати, Яндекс разрабатывает открытую библиотеку машинного обучения CatBoost, работающую “из коробки” с категориальными признаками (в отличии от остальных алгоритмов, которые не могут переварить понятия “панель”, “кирпич” и “монолит” в чистом виде, требуя кодировать слова цифрами, CatBoost ест датасет сразу со словами внутри!).

У всех алгоритмов под капотом есть куча настроек, существенно влияющих на результат, но об этом позже.

Небольшие данные

Итак, алгоритму нужны данные. Я не нашел ничего лучше, чем парсить ночами Циан, собирая объявления о продаже квартир на вторичке Москвы за три последних месяца (самые свежие предложения — январь 2020 года).

При очистке данных избавился от дублей, объявлений с одинаковым текстом в описании, от строк с нулевой площадью кухни, без материала стен и этажей в доме. В итоговом датасете примерно
64 000 строк, жить можно!

Придется чуть забежать вперед и рассказать про фичи. Так датасайентисты называют признаки (свойства) примеров из датасета. В нашем примере фичи — это как раз площадь, этаж, материал стен, местоположение, то есть те параметры, по которым будет происходить предсказание. В оценочном обиходе — ценообразующие факторы.

В изначально спарсенном датасете у меня было много фич — все, что смог вытащить с Циана: количество комнат, год постройки, материал стен, этаж квартиры и этажность здания, общая площадь, размер кухни, широта и долгота расположения дома, адрес, округ, тип продажи, возможность ипотеки, лифты в здании.

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

Еще есть понятие “фича-инжиниринг”. Это когда мы на основе имеющихся фичей (модные ребята ставят ударение на последний слог) генерируем новые. Я долго колдовал с широтой и долготой, пытаясь сообщить модели нечто важное о местоположении (пресловутый локейшн!), пока не сообразил вычислить два принципиально новых признака: расстояние от центра города и азимут (угол относительно направления на север, в градусах). Бинго! Точность существенно выросла!

Датасет в формате csv можно посмотреть по адресу: https://raw.githubusercontent.com/maxbobkov/ml_moscow_flats/master/moscow_dataset_2020.csv. С ним и будем работать дальше.

Ближе к делу

Код будем писать на языке Python (назван в честь “Летающего цирка Монти Пайтона”, а вовсе не в честь змеи, как можно подумать).
Он легкий и понятный для новичков. Никаких безумных скобок, ручного выделения памяти и строгостей вроде “если эта переменная содержит в себе целое число, то ей нельзя присвоить текстовое значение”. В пайтоне можно! Кроме того, он сейчас де-факто стандарт в сфере анализа данных и для него написана масса библиотек для машинного обучения. Снобы считают пайтон медленным языком (по сравнению с Си, но для наших целей это не критично).

https://xkcd.ru/353/

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

В процессе будем использовать библиотеки:

  • Pandas — это такой эксель для программистов)) мощнейшая работа с табличными данными, их изучение, комбинирование, фильтрация и т.п.;
  • Scikit-learn — библиотека, содержащая массу инструментов для машинного обучения, от разбиения датасета на выборки, до построения лесов и вычисления метрик;
  • Matplotlib — позволяет строить графики и прочие визуализации;
  • Geopy — вычисляет расстояния между географическими точками;
  • XGBoost — тот самый алгоритм бустинга, о котором шла речь ранее.

Экспериментировать будем в сервисе Google Colab. Он бесплатен, работает в браузере и дает для расчетов GPU — мощный графический процессор NVIDIA Tesla K80, благодаря которому можно кратно ускорить обучение моделей. И там есть Jupyter Notebook.

Jupyter Notebook — оболочка языка Python в которой можно писать код блоками, запуская их поэтапно. Это не очень годится для полноценного программирования, зато удобно для научных расчетов — блоки кода можно перемежать с текстовыми вставками, графиками и т.п., все на одном листе и наглядно.

Для доступа к Colab нужно иметь логин в Гугле и браузер, ничего устанавливать на компьютер не придется. Перейдите по адресу: https://colab.research.google.com/ и залогиньтесь (синяя кнопка Sign In в правом верхнем углу).

Появится окно с вкладками Example, Recent, Google Drive, GitHub, Upload, выберите GitHub (Если не появилось, используйте меню File >Open notebook). В строку поиска вставьте адрес: https://github.com/maxbobkov/ml_moscow_flats/blob/master/moscow_flats_models.ipynb и нажмите Enter:

Colab найдет ноутбук с заранее подготовленным кодом на гитхабе и останется только кликнуть на надпись moscow_flats_models.ipynb справа от логотипа с котиком (на самом деле это не котик)):

Ноутбук загрузится и будет готов к работе. Единственное, подключите графический ускоритель (GPU), пройдя в меню Edit > Notebook Settings > Hardware accelerator, выберите в выпадающем меню GPU вместо None и нажмите Save. Все готово к экспериментам!

Поехали!

Код в ноутбуке разбит на блоки, будем запускать их по порядку. Для исполнения блока встаньте курсором внутрь кода и нажмите на кнопку с треугольником в белом круге, эдакая команда Play (Или нажмите Ctrl+Enter).

В первый раз система ругнется “Warning: This notebook was not authored by Google.”, жмите RUN ANYWAY. Будь, что будет)) Во время исполнения кнопка приходит в движение, на первых блоках это быстро, когда начнется обучение, придется подождать.

Подключаем библиотеки командой import:

Функции — концепция, фантастически упрощающая программирование. Если один и тот же код нужно использовать многократно, мы не пишем его сто раз, а выносим в функцию, записанную определенным образом с инструкцией def, именем функции и аргументами, которые она получает на вход. Потом в нужном месте ее можно вызвать через ее имя, указав конкретные значения аргументов. Я вынес в функции расчет азимута на основании координат центра города и жилого дома, а также расчет метрик и коэффициента детерминации (знаменитый r-квадрат, который, чем ближе к единице, тем лучше модель описывает реальность).

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

Следущий шаг — загрузка датасета. В код вшит адрес, по которому хранятся данные, программа подтянет их. В дело включается библиотека Pandas, она читает csv-файл и преобразует его в свой собственный формат данных — датафрейм (по сути — таблицу), который мы сохраняем в переменной df.

Последняя строка кода df.head(5) выводит 5 первых строк таблицы, чтобы визуально оценить, какой информацией мы располагаем.

А теперь работаем с датасетом, создавая дополнительные признаки и столбцы: цену 1 квадратного метра квартиры (именно это значение мы будем предсказывать), расстояние от центра города и азимут (обратите внимание, что вызывается функция get_azimuth(), объявленная ранее).

На основании вычисленных расстояний отбираем для работы только те предложения, которые расположены не дальше 40 км от центра Москвы, что как раз покрывает территорию Новой Москвы, но отсекает неадекватные варианты, явно расположенные в области. Округляем значения некоторых столбцов и на выходе видим, что признак wallsMaterial имеет тип object (не является числовым, в отличие от остальных), с этим нужно что-то делать, чтобы модель могла работать с материалом стен.

Мои опыты показали, что качество моделей повышается, если отсечь от датасета выбросы, поэтому жертвуем 3 000 строк, значения которых сильно выбиваются из общей картины.

Заменяем текстовые значения в столбце wallsMaterial числовыми при помощи функции LabelEncoder() библиотеки Scikit-learn, результат ее работы — ниже. В первой строке вывода результатов видно на какое именно число заменен определенный тип стен, это знание пригодится дальше.

Последний шаг подготовки к обучению — выделение целевой переменной, той, которую мы будем предсказывать. Традиционно обозначается через переменную y (игрек) и ей присваивается столбец датафрейма “Цена квадратного метра”.

Затем отбираем фичи, которые будут участвовать в обучении модели и формируем новый датасет X, содержащий столбцы с характеристиками, но не содержащий цен.

В последней строке функция train_test_split() автоматически разбивает X и y на четыре группы — тренировочный Х, валидационный (проверочный) Х, тренировочный y, валидационный y. Это позволит нам проверить качество модели на незнакомых ей данных.

Пора обучить первую модель, основанную на алгоритме случайного леса деревьев решений. Создаем объект RandomForestRegressor (ранее импортированный из библиотеки Scikit-learn) и задаем ему ряд настроек (т.н. гиперпараметров), в частности, количество деревьев: 2000 (рощица)), максимальную глубину дерева: 55 уровней и пр., сохраняем его как rf_model. Опущу способы подбора оптимальных гиперпараметров, чтобы не перегружать материал, но в случае интереса гуглите “RandomizedSearchCV”.

Ноутбук уходит на некоторое время в себя, строит деревья и оптимизирует их. Когда модель обучится (за это отвечает метод fit, который применяется к созданной модели rf_model), метод predict предскажет цены для тестовой выборки квартир (val_X). А созданная нами функция print_metrics() получит на вход предсказанные цены и известные нам заранее “правильные”, и напечатает значения метрик. Неплохой r-квадрат и медианная ошибка! Идем дальше.

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

Есть еще один способ попытаться улучшить прогноз — усреднить предсказания моделей. В нашем случае это привело к уменьшению средней ошибки до 7.72% при коэффициенте детерминации 0.82. Медианная ошибка: 5.21%. Что ж, далеко до эталонного zillow.com (немного статистики про точность их моделей: https://www.zillow.com/zestimate/), но терпимо для индикативно-прикидочных целей. Кстати, сам Циан заявляет, что в среднем, цена сделки на 8% ниже цены предложения.

Из интересного — есть способ посмотреть важность признаков при построении случайного леса. К вопросу о локейшене — половину ценной информации для оценки несет на себе расстояние от центра города. Следующий по важности — азимут, что логично, не все направления одинаково полезны. Остальные признаки примерно равнозначны, но тоже сделали свой вклад в точность модели. Для сравнения — отброшенные мной признаки: тип отделки, возраст дома, тип продажи — давали вклад порядка 0.5–1%.

А теперь самое интересное. Попробуем оценить единичную квартиру — двушку в панельном доме, выставленную на Циане за 11 700 000 рублей.

Для этого создаем датафрейм из одной строки, описывающий параметры этой квартиры (широту и долготу можно взять на Яндекс.Картах, а wallsMaterial— это одна из цифр, которыми закодированы типы стен, см. выше по тексту), делаем знакомый нам расчет дополнительных фич, прогнозируем цену предложения по обоим построенным моделям (метод predict для ранее натренированных моделей rf_model и xgb_model), усредняем результат и умножаем на площадь квартиры (мы ведь предсказывали цену метра).

11 683 000 рублей против 11 700 000 в объявлении. Магия)

Спасибо за внимание

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

Играйте с этим кодом, меняйте гиперпараметры, набор фич для обучения, это затягивает! Только держите в голове, что Colab дает рабочее пространство на время, часов через 12 ноутбук превращается в тыкву и его нужно загружать с гитхаба заново. И если что-то пошло не так, используйте меню Runtime > Restart runtime, чтобы обнулить все расчеты и начать заново с первого блока кода.

Методы, которые я продемонстрировал, пока мало применимы в нашей оценочной практике, потому что Стандарты и проверяющие органы привыкли к эксельке с тремя аналогами. А что делать со строчками кода, которые только что переварили 60 000 аналогов и выплюнули какой-то результат не очень понятно.

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

Коллеги, если у вас накопились какие-то интересные данные, с которыми можно поэкспериментировать, или есть желание копнуть тему глубже, моя почта max.bobkoff@gmail.com, буду рад единомышленникам!

--

--