Введение
Начну с конца. Это скриншот с некой web-карты, визуализирующей среднюю стоимость недвижимости на вторичном рынке Саратова и Энгельса:
Цвета на карте можно соотнести с цветами на «легенде», цвет на «легенде» соответствует средней стоимости квадратного метра общей площади в тысячах рублей.
Точка на карте соответствует одному предложению по продаже (на вторичном рынке) квартиры с Авито. Всего таких точек, как видно на «легенде», для построения графика использовалось 4943.
Карта в интерактивном виде доступна на GitHub.
А теперь немного предыстории..
Давным-давно…
… когда будучи студентом мехмата я писал работу по статистической обработке данных по ценам на недвижимость, я случайно нашел полезный документ, — градация районов по ценам на недвижимость, что-то вроде “Ценовые зоны Саратова”. Не знаю, кто ее составлял, но составлена она была правдоподобно, — анализ показал, что (если учесть другие факторы), разница между средней стоимостью квадратного метра квартир из соседних ценовых зон составляла примерно 10%. С тех пор выступать в роли оценщика недвижимости (на бытовом уровне), как и большинству взрослых людей, приходилось не раз.
Идея снова подойти к этому вопросу более “научно” пришла в голову когда я увидел публикацию Jeff Kaufman Boston Apartment Price Maps.
Товарищ сделал следующее:
- Python скрипт, который парсит с сайта PadMapper данные по стоимости аренды недвижимости.
- еще скрипт, который используя методы Spatial interpolation, визуализирует эти данные в виде растрового изображения.
- Наложил этот рисунок на страничку с Google Maps
В результате у него получилась такая интерактивная карта распределения стоимости аренды на карте Бостон-а:
После первого “Вау, прикольно”, следующая мысль — “А что если попробовать сделать такое же, но для России, только не по стоимости аренды, а по ценам на квадрат недвижимости”.
Сразу оговорюсь, я, по текущему роду деятельности, скорее database developer, и не являюсь специалистом ни по GIS данным, ни по front-end разработке, ни по статистике, ни по Python, поэтому отнеситесь снисходительно к… еще не знаю к чему, но надеюсь этого будет не очень много и вы мне об этом напишете для исправления.
И здесь возникает первая крупная задача
Web Scraping с Авито.
Так как с Python-ом на тот момент я был совсем на “вы”, после изучения HTML кода страниц Авито, свой собственный велосипед я решил написать в виде библиотеки на C#, используя для парсинга AngleSharp.
Не буду сейчас углубляться в особенности реализации, замечу только что:
- все метаданные парсинга Авито я вынес в отдельный конфигурационный класс, чтобы в будущем, при изменении структуры разметки на Авито, можно было бы просто изменить строку в конфигурационном файле.
- дабы избежать бана от Авито, пришлось добавлять искусственные задержки в 10 секунд после обработки каждого объявления. (Согласен, — не самый умный способ, но найти подходящий пул прокси -айпишников навскидку не удалось).
В конечном итоге, запустив парсилку на ночь, я получил нужные данные, записанные в табличку в базе
Очистив данные от “Старого фонда”, “Элитных”, и всяких явно неадекватных предложений, я получил выборку из пяти тысяч точек — продажи квартир на вторичном рынке Саратова и Энгельса. И это дало возможность перейти к следующему шагу:
Обработка GIS данных.
Общая постановка задачи:
— есть дискретный набор значений некой метрики(температура, высота, концентрация вещества и т.п), с привязкой к координатам на плоскости (X, Y или широта, долгота),
— нужно иметь возможность предсказать значение этой метрики в произвольной точке этой плоскости,
— результат визуализируется в виде цветовой схемы на плоскости. Для цветовой схемы для удобства можно дискретизировать цвета.
Пример, — сняли в один момент показания с датчиков температуры в разных местах, обработали, получили такую картинку:
На самом деле, задачи такого рода требуют погружения в тему “Spatial interpolation”. Здесь есть свой математический аппарат, например:
— Inverse Distance Weighting (IDW) Interpolation
— Kriging
И есть свой инструментарий в составе популярных GIS пакетов, например
— Интерполяция точечных данных qGIS
— GeoStatistical Analyst ArcGIS
— Use of SAGA GIS for spatial interpolation(kriging)
— Kriging in R.
IDW в составе qGIS (см первую из ссылок) я попробовал, — результат меня не удовлетворил.
Поэтому, чтобы не завязнуть надолго в освоении нового инструментария, я предпочел воспользоваться готовым Рython скриптом от Jeff Kraufman . Для расчета расстояний и для трансляции GPS координат в X,Y координаты на растре здесь применяется упрощенная линейная формула, без применения используемой в Google MapsWeb Mercator проекции.
Насколько я понял, в скрипте используется вариация на тему Inverse Distance Weighting (IDW), главная (и самая тяжеловесная по расчетной части) в этом скрипте часть здесь в этом коде.
gaussian_variance = IGNORE_DIST/2
gaussian_a = 1 / (gaussian_variance * math.sqrt(2 * math.pi))
gaussian_negative_inverse_twice_variance_squared = -1 / (2 * gaussian_variance * gaussian_variance)
def gaussian(prices, lat, lon, ignore=None):
num = 0
dnm = 0
c = 0
for price, plat, plon, _ in prices:
if ignore:
ilat, ilon = ignore
if distance_squared(plat, plon, ilat, ilon) < 0.0001:
continue
weight = gaussian_a * math.exp(distance_squared(lat,lon,plat,plon) *
gaussian_negative_inverse_twice_variance_squared)
num += price * weight
dnm += weight
if weight > 2:
c += 1
# don't display any averages that don't take into account at least five data points with significant weight
if c < 5:
return None
return num/dnm
def distance_squared(x1,y1,x2,y2):
return (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)
Подогнал свой массив данных под нужный формат, убрал из скрипта ту часть, которая делает расчет корреляций относительно bedroom_category (количество комнат), поигрался с входными параметрами, запустил расчет. На моем компе (Intel® Core(TM) i7-6700 CPU, 16GB RAM), расчет с 5 тыс точек на входе и разрешением изображения на выходе 1000*1000 занял примерно полчаса.
Осталось немного поработать с Front-End -ом. Смотрим HTML source карты Бостона от Jeff Kraufman. Немного видоизменяем его под свои данные, и получаем вот такую «картину маслом».
Для проверки на явные “косяки” в Georeferencing загружаем тот же набор точек в qGIS, подгружаем ГуглоКарты, сравниваем картинки.
Убедившись, что точки на нарисованной карте находятся на своих местах, а цвета на ней правдоподобно отражают цены на квартиры в нашей деревне, можем выдохнуть — “Yes! Работает!”.
Только вот немного огорчает, что картинка у нас залезла в Волгу. Что естественно, — алгоритм расчета ничего про Волгу не знает.
Стало быть возникает задача:
Обрезать изображение на карте по береговой линии
Попытка найти гео-данные по административным границам городов с учетом береговой линии не увенчалась успехом. Есть Саратов, есть Энгельс, и граница между ними проходит посреди Волги. Ладно, мы пойдем другим путем.
Скачиваем из открытых источников shape file c данными по береговым линиям, загружаем как векторный слой в qGIS, выбираем нужные полигоны, разбираемся в структуре файлов, вытаскиваем полигоны, которые относятся к Волге у Саратова, заливаем данные в БД.
Немного магии типа:
DECLARE @Volga geography;
select @Volga= PlacePolygon.MakeValid() from PlacesPolygons where PlacesPolygonID =1003
DECLARE @Saratov geography;
select @Saratov= PlacePolygon.MakeValid() from PlacesPolygons where PlacesPolygonID =2
select @Saratov.STDifference(@Volga)
И мы получаем нужный полигон — границы города с учетом береговой линии.
Остается загрузить это в qGIS, загрузить туда же сгенерированную скриптом картинку как растровый слой, провести Georeferencing для этой картинки, и обрезать ее по полигону с границами городов, все стандартными средствами qGIS. Все было бы здорово если бы… мне это удалось сделать. Но увы… с Georeferencing в qGIS у меня отношения не сложились, и после нескольких попыток я решил, что используя spatial функции из Microsoft.SqlServer.Types, проще быстренько сделать очередной велосипед в виде утилиты на шарпе.
string GPSPolygonVolga = " POLYGON ((46.2324447632 51…. ))";
private void CropByGPS()
{
Bitmap bmp = new Bitmap(inputFilePath);
int w = bmp.Width;
int h = bmp.Height;
var GpsSizeOfPyxelX = (MAX_LON - MIN_LON) / w;
var GpsSizeOfPyxelY = (MAX_LAT - MIN_LAT) / h;
var VolgaPolygon = SqlGeography.STPolyFromText(new System.Data.SqlTypes.SqlChars(GPSPolygonVolga), SRID);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
Color c = bmp.GetPixel(x, y);
var GPS = SqlGeography.Point(MAX_LAT - GpsSizeOfPyxelY * y, MIN_LON + GpsSizeOfPyxelX * x, SRID);
if (VolgaPolygon.STContains(GPS)) //попадает ли точка в "водяной" полигон
bmp.SetPixel(x, y, Color.Transparent);
}
}
bmp.Save(outputFilePath, System.Drawing.Imaging.ImageFormat.Png);
}
Загрузили исходный файл с растром, получили на выходе тот же файл, но в немного “обкусанном виде”. Наложили на Google Maps в браузере, смотрим, — на Волгу ничего не залезает, отлично! Смотрим чуть внимательнее и видим… что кажется кое-где откусили лишнее. Косяк в данных или косяк в процедуре? Снова смотрим на загруженные слои в qGIS.
Да… действительно, кое-где у нас порой… полигоны из слоя водоема явно залезает в город. Прям не Энгельс, а Венеция какая-то… Ладно, нет стремления к совершенству, мы пойдем другим путем.
Раз векторные данные вышли из доверия, будем работать с растровыми. Нам нужно наложить две картинки, и вырезать из первой те точки, которые соответствуют “голубому” во второй.
Картинку с картой нам могут дать, например Google Maps static API.
Получаем API_KEY, выясняем, что:
— максимальное разрешение для бесплатного использования =640*640
— параметризации по BoundingBox -, в отличие от BING static maps API гугл не поддерживает, и придется дополнительно соотносить два рисунка в координатной плоскости.
Ладно, пробуем. Получаем от Google Statics Map API некую картинку с картой, покрывающую интересующую нас область, пишем такой код:
private static void CropByMapImage()
{
Bitmap bmp = new Bitmap(inputFilePath);
Bitmap bmpMap = new Bitmap(mapToCompareFilePath);
int w = bmp.Width;
int h = bmp.Height;
var GpsSizeOfPyxelX = (MAX_LON - MIN_LON) / w;
var GpsSizeOfPyxelY = (MAX_LAT - MIN_LAT) / h;
int wm = bmpMap.Width;
int hm = bmpMap.Height;
var cntAll = w * h;
var cntRiver = 0;
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
var currentLatitude = MAX_LAT - GpsSizeOfPyxelY * y;
var currentLongitude = MIN_LON + GpsSizeOfPyxelX * x;
if (currentLongitude < MIN_LON_M || currentLongitude > MAX_LON_M || currentLatitude < MIN_LAT_M || currentLatitude > MAX_LAT_M) continue; //todo - something with this ugly code
var CorrespondentX = (int)((currentLongitude - MIN_LON_M) / (MAX_LON_M - MIN_LON_M) * wm);
var CorrespondentY = (int)((MAX_LAT_M-currentLatitude) / (MAX_LAT_M - MIN_LAT_M) * hm);
Color cp = bmp.GetPixel(x, y); //color of current pixel
Color mp = bmpMap.GetPixel(CorrespondentX, CorrespondentY); //color of correspondent Map pixel
if (Math.Max(Math.Max( Math.Abs(mp.R - WaterMapColor.R) , Math.Abs(mp.G - WaterMapColor.G)) , Math.Abs(mp.B -WaterMapColor.B))< colorDiff /* && cp.A != Color.Transparent.A */)// blue water is not always the same blue
{
bmp.SetPixel(x, y, Color.Transparent);
cntRiver++;
}
}
}
bmp.Save(outputFilePath, System.Drawing.Imaging.ImageFormat.Png);
}
Обрабатываем наш файлик с разрешением 1000*1000 с помощью файлика с разрешением 640*640, получаем… такой результат (здесь для наглядности те точки, которые должны быть Transparent, сделаны желтыми):
Результат — печальный, в том числе еще и потому, что Гуглокарта масштабируется дискретно, и поэтому с большим запасом перекрывает наш сгенерированный растр. То есть или нужно брать для “обрезки” много карт с подходящим масштабом или… как то все таки найти одну, но с хорошим разрешением.
Я — пошел вторым путем, то есть просто за-скриншотил нужную проекцию обычной гуглокарты из браузера. GPS координаты углов (mapping bound), я узнал, добавив в JavaScript code такое:
google.maps.event.addListener(map, 'bounds_changed', function () {
console.log(map.getBounds());
Обработал наш растр с помощью новой карты из сохраненного скриншота, загрузил в браузер и… результат меня — скорее порадовал. Достаточно корректно(в пределах точности GPS позиционирования) “зачистился” даже маленький пруд в местном городском парке.
Все, будем считать первый этап квеста завершенным, заливаем результаты на GitHub в папочку, расшаренную для хостинга, и можем хвастаться перед IT-сообществом.
И что дальше?
А дальше возникает много вопросов, на которые я пока не знаю четкого ответа:
- Насколько корректно работает алгоритм расчета здесь по сравнению с другими методами spatial interpolations
- Какие еще факторы, кроме расположения объекта недвижимости, имеет смысл учитывать при расчете и как ввести в расчетную модель
- Можно ли сделать постоянное обновление данных в базе и построить на этом какую то систему оценку недвижимости. (Если меня читают на этом сайте представители Авито, хотелось бы услышать комментарии)
Ну и… спасибо всем дочитавшим до этого места. Мне было интересно решить практическую задачу, которая оказалась, “rather challenging for me”, надеюсь вам было интересно прочитать о моем опыте.
Комментарии (34)
pudovMaxim
22.03.2017 19:42+1Немного подпилить напильником, набрать больше статистики и будет совсем хорошо.
Очень полезно в изучении цен в других городах/регионах. Т.к. в родном городе обычно сам понимаешь где дороже/дешевле и знаешь тому причины. А подобная карта поможет сразу заподозрить неладное, если где-то в районе жилье будет намного дешевле.
jerry_kiwi
22.03.2017 21:04http://www.spatial-econometrics.com/ — вот тут про факторы
а по поводу картинки — красивая. но не более.
самое интересное — насколько разбегаются значение в точках от фактических.
без погружения в тему — есть опасность получить красивые картинки, далёкие от реальности ;(
http://www.spatialanalysisonline.com/HTML/index.html
dSailor
22.03.2017 21:04Крутая идея! Пилите старт-ап!!!
Самая ценная, в данном варианте информация, это:
1) С какой скоростью меняются цены (то есть нужно привязывать дату обновления цен)
2) В какую сторону движется «циклон» цен. (Инвестору было бы важно увидеть информацию, по изменению цен в каких то определенных районах.
Особенно было бы здорово применительно к земле!
Как только прошел ГАЗ… сразу цена растет пропорционально.
Соответственно, глядя на Вашу инфу за последний год (с изменениями цен 1 раз в месяц), я бы увидел в каком районе приоритетно купить землю=)
Думаю эта разработка была бы интересна всем риэлторским конторам.dklmn
22.03.2017 21:17+1Чтобы увеличить массив входных данных «вширь» (по другим регионам) и «вглубь» (хранить историю), нужно сначала решить вопрос с баном на Авито.
Можно попробовать техническими способами — например через пул прокси-айпишников, но лучше организационными — через договоренности с Авито.
atikhonov
22.03.2017 22:36+1Можно прогнозировать модельную цену квартир (подобно)
а на карте цветом уже отображать разницу или отношение, модельной цены к цене продажи,
и, возможно, появятся кластеры областей пере-/недооцененных квартир,
и по ним будет понятно, что жители знают больше о своем доме-районе (позитивное или негативное)dSailor
22.03.2017 22:46По Вашей ссылке можно понять среднюю стоимость квартиры и узнать преувеличивает или занижает собственник цену. А на карте возможно увидеть перспективные районы для инвестирования, с целью покупке с неплохим дисконтом.
От правительства как правило напрямую, заблаговременно, не узнать, в какую сторону пойдет метро (москва) например лет через 5-10, а по анализу роста стоимости квартир это можно определить=) или хотя бы проводить анализ в направлении конкретного района, опять же на основании полученных с карты данных.
А то что в целом можно было бы использовать и ту и ту инфу, это конечно было просто бомбой для риелтора=)atikhonov
22.03.2017 22:59А Ваша идея про динамику цен очень интересная и полезная:
ежемесячный парсинг цен, и создание анимированного гифа на карте,
будет очень показательно: как в общем тренде («теплеет» или «холодеет» район/город/страна),
так и в выявлении очагов интереса/равнодушия к конкретному району/городу.
dmitryvakarev
23.03.2017 10:23+2На самом деле, если делать бизнес, то не всё так просто. Писал прогу, которая парсила большой сайт с вариантами квартир, отслеживал изменения цен, отображал появление новых квартир в топе, сильные падения цен в топе, поисковик с кучей параметров, всё это разными цветами, можно отслеживать изменения цен квартиры, дома, улицы. И еще куча фич. И казалось бы полезная программа, договорился с риэлторами. В итоге, когда прошло много времени, им стало как бы это не интересно, так как у них своя база. Да и директору агентства важно, чтобы было больше клиентов больше денег, а то как будет искать менеджер его не сильно волнует. В общем я бы посоветовал поискать толкового риэлтора, который что-то посоветовал, затем найти тех кто бы приобрел продукт, а лучше крутого менеджера, который впарит эту фишку директорам агентств, так как самому впаривать достаточно проблематично.
JustFailer
22.03.2017 21:05Очень полезно, спасибо :)
Планируя переезд тоже подумывал написать что-нибудь подобное для оценки недвижимости в разных районах города. Поделиться исходниками как автор карты Бостона не планируется?dklmn
22.03.2017 21:21Исходники чего? Парсилки для Авито? Там много кода, по которому рефакторинг плачет, поэтому не стал выставлять ссылку на репозиторий. Если есть интерес, могу лично выслать.
multed
23.03.2017 10:20+2Надо еще не забывать, что получилась карта стоимости предложения. А цена сделки, часто, отличается от предложения процентов на 10-15.
Так что красиво, да, какую-то ценность имеет. Но это не сам рынок.dklmn
23.03.2017 10:21+1Так то да.
Но доступа к данным регистрационной палаты у меня нет, так что приходится дейстовать по принципу «я его слепила из того что было».multed
23.03.2017 10:34Это понятно, что данных таких нет. Ну и через регпалату не всегда точные суммы проходят.
Еще вариант — как-то дружить с риелторами и получать обезличенные суммы сделок по районам от них.
multed
23.03.2017 12:51и еще пять копеек по теме. цена за квадрат даже в одном районе сильно разная у разного типа домов. хрущевка там или новые дома с модной планировкой. эти дома могут стоять рядом, а ценник при этом очень отличаться. средняя цена будет не совсем корректной.
atikhonov
23.03.2017 14:25а может тогда и не надо было усреднять, а помещать только цветные или с разной степени интенсивности (прозрачности) точки, по одному адресу (координате) редко очень много объявлений, и сразу будет видно что и как, плюс зум же есть.
dklmn
23.03.2017 22:17Понятно, что для того, чтобы улучшить модель, нужно исследовать и вводить еще как минимум ряд факторов:
— год постройки дома
— тип дома для домов типовых серий (например, 90 серия )
— Количество комнат
— Первый или последний этаж
— Деньги вложенные в ремонт.
Вот только последний фактор наверно влияет на общую дисперсию сильнее всего, а получить по нему какую то числовую оценку — фактически невозможно.
Кстати, подскажите если кто знает, есть ли сейчас возможность получить открытые данные по году постройки дома по адресу?
claygod
Глядя на иллюстрации, скажу что Вы вполне защитили дипломную работу по картографии )
Если говорить о практической стороне дела, то я бы хотел видеть такие карты на крупных сайтах по продаже недвижимости. Но нужно делать две версии — отдельно новостройки, отдельно вторичка.
Ordinatus
Можно еще добавить по коммерческой недвижке, земле под застройку, загородной недвижке и т.д. короче даешь слои на карту много и разных!
ads83
Аналогичная карта есть у Этажей https://saratov.etagi.com/my/analytics/ (внизу страницы, прямую ссылку на карту не нашел).
С разбивкой по районам города (что важно), но без деталей внутри них (что хуже).
Я не связан с агентством и не знаю, как строится их карта. Очевидно что на базе Яндекс.Карт, но считается ли средняя цена на лету?
P.S. На мой вкус, у автора карта посиматичнее :)
dklmn
Спасибо за ссылочку.
Только, мне кажется термин «Тепловая карта» здесь не совсем корректен, под Heat map обычно понимают, насколько я понимаю, не распределениие цифровой метрики, а распределение «наличие факта».
Например Heat map of London crime
atikhonov
не совсем так, тепловая карта это отображение численной характеристики цветом (не важно плотность это или просто какая-то метрика) на координатной системе факторных переменных,
но вполне допустимо употреблять этот термин и для обычных карт.
dklmn
Да, наверно вы правы по терминологии. Просто у Гугла The Heatmap Layer provides client side rendering of heatmaps. — это про распределение плотности.
atikhonov
Так у них по районам (а все объявления привязаны к районам) и широченные интервалы,
один раз построили такую карту и несколько лет ее и менять не надо;)
Pakos
Один разрез — продажа/аренда, да и по количеству комнат (студии(? скорее как однушки идут),1,2,3,4+) тоже неплохо было, вполне может быть что карты будут сильно меняться в зависимости от числа комнат. Только застройщики теперь всячески изгаляются: «евродвушка» — по факту однушка с большой кухней (т.е. как «евро» тянет максимум как 1 bedroom) и могут подпакостить данным.
Хотя как практическое… никогда не искал квартиру, опираясь на цену района, всегда смотрел «нравится» по районам, потом отфильтровав по «хватает», а потом уже «стоит посмотреть объект». Разве что если ехать в новый город и смотреть «где бы пожить, пока обвыкнусь и пойму стоит ли оставаться», но это аренда.
PS. Про анализ выбивающихся цен не подумал — это может пригодиться любому, кто не в теме или отстал от жизни, т.к. давно не занимался темой.