Сбор данных
С постановкой задачи понятно, возникает вопрос, откуда же брать данные, в РФ есть несколько основных сайтов по поиску недвижимости, и есть база WinNER, которая в достаточно удобном интерфейсе содержит максимальное количество объявлений и позволяет осуществлять выгрузку в csv формате. Но если раньше данная база позволяла повременной доступ (минуты, часы, дни), то теперь минимальный только 3 месячный доступ, что является несколько избыточным для обычного покупателя или продавца (хотя если серьезно подходить, то можно понести эти расходы). С этим бы вариантом было бы совсем просто, поэтому пойдем другим путем, парсингом с какого-нибудь сайта недвижимости. Из нескольких вариантов, я выбрал наиболее удобный небезызвестный cian.ru. На тот момент, отображение данных представлялось в простой табличной форме, но когда я попытался распарсить все данные на R, тут меня постигла неудача. В некоторых ячейках, данные могли быть недозаполнены, а стандартными средствами функций R я не мог зацепиться за якорные слова или символы, и надо было или использовать циклы (что в R для такой задачи некорректно) или использовать регулярные выражения, с которыми я совсем не знаком, поэтому пришлось использовать альтернативу. Этой альтернативой оказался замечательный сервис import.io, который позволяет извлекать информацию со страниц (или сайтов), имеет свое REST API и отдает результат JSON. А R может и запрос к API осуществить и JSON разобрать.
Быстро освоившись с данным сервисом, создал на нем экстрактор, который извлекает всю требуемую информацию (все параметры каждой квартиры) с одной страницы. А R уже перебирает все страницы, вызывая данный API для каждой и склеивая получаемые JSON данные в единую таблицу. Хотя import.io позволяет сделать и полностью автономный API, который проходил бы по всем страницам, я посчитал, что логично, чего я не могу сделать (парсинг одной страницы) правильно возложить на стороннее API, а все остальное продолжать делать в R. Итак, источник данных выбран, теперь об ограничениях будущей модели:
- Город Москва
- Один тип квартир в модель (то есть однушки или двушки или трешки)
- В пределах одной станции метро (так как используется геокодирование, и расстояние до ближайшего метро)
- Вторичка (так как новостройки все принципиально разные, и качественно друг от друга отличаются, а в объявлениях это либо не сказано, или написано что-то в комментариях, но ни то, ни другое, не позволяет создать адекватную модель)
Обзор данных
Как это часто бывает, в данных могут оказаться и выбросы, и пропуски, и просто обман, когда новостройки выдают за вторичку, или вообще продают землю.
Поэтому первоначально надо привести все данные к «опрятному» виду.
Проверка на обман
Здесь основной упор на описание объявления, если встречаются слова явно не относящиеся к требуемой мне выборке, эти наблюдения исключаются, это проверяется функцией R grep. А так как в R вычисления векторные, данная функция сразу возвращает вектор значений верных наблюдений, применив его, отфильтруем выборку.
Проверка на пропущенные значения
Пропущенные значения встречаются достаточно редко (и в основном именно в «левых» объявлениях, с новостройками и землей, которые к этому моменту уже исключены), но с теми, что остаются надо что-то делать. Вариантов, в общем-то, два, исключать такие наблюдения или заменять эти пропущенные значения. Так как жертвовать наблюдениями не хотелось, а исходя из предположения, что данные не заполнены исходя из принципа «что его заполнять, и так понятно, что это как все окружающее», решил заменять качественные переменные на моду их значений, а количественные (метраж) на медианные значения. Конечно, это не совсем корректно, и совсем правильно было бы проводить корреляционное отношение между наблюдениями, и заполнять пропуски согласно полученным результатам, но для данной задачи посчитал это избыточным, притом таких наблюдений достаточно мало.
Проверка на выбросы
Выбросы встречаются еще реже, и могут быть только количественными, а именно по цене и по метражам. Здесь исходил из предположения, что покупатель (я) конкретно знает по какой цене и примерно какого метража должна быть его квартира, поэтому задавая начальные значения верхней и нижней цены, ограничивая метраж, мы автоматически избавляемся от выбросов. Но даже если это не так (не делать ограничения), то при получении результата или взглянув на диаграмму рассеяния и увидев, что есть выброс, можно осуществить запрос с уточненными данными, тем самым убрав эти наблюдения, что улучшит модель.
Основные предпосылки теоремы Гаусса-Маркова
Забегая вперед, скажу, что от модели мы не хотим получения трактовки коэффициентов, или корректной оценки их доверительных интервалов, она необходима нам для прогнозирования «теоретической» цены, поэтому некоторые предпосылки нам не критичны.
- Модель данных правильно специфицирована. В общем да, путем исключения выбросов, некорректных объявлений, замены пропущенных значений, модель вполне адекватна. Какая-то нестрогая мультиколлинеарность может присутствовать (например пятиэтажка и нет лифта или площади – общая/жилая), но как писал выше, для прогнозирования это не критично, более того она не нарушает и основные предпосылки. Для целей построения тестовых моделей, все значений переводились в дамми-переменные корректно, строгая мультиколлинеарность исключена.
- Все регрессоры детерминированы и не равны. Да, тоже верно.
- Ошибки не носят систематического характера. Верно, так как в МНК используется свободный член, которые уравнивает ошибки.
- Дисперсия ошибок одинакова (гомоскедастичность). Так как используется ограничения по размеру регрессоров и зависимой переменной (масштаб сопоставимый), то гетероскедастичность минимальна, да и для прогнозирования она снова не критична (стандартные ошибки несостоятельны, а они нам не интересны)
- Ошибки некоррелированы (эндогенность). Вот тут нет, эндогенность, скорее всего, есть (например, квартиры -«соседи» с одной площадки или подъезда), есть какой-то внешний неучтенный фактор, но снова для прогнозирования, запись с эндогенностью не принципиальна, более того, мы не знаем этот неучтенный фактор.
Данные в целом соответствуют предпосылкам, поэтому можно строить модели.
Набор регрессоров
Помимо переменных (получаемых непосредственно с сайта), решил добавить дополнительные регрессоры, а именно пятиэтажный дом или нет (так как обычно это весьма качественное отличие), и расстояние до ближайшего метро (аналогично). Для определения расстояния используется API сервиса геокодирования Google (выбрал его как наиболее точный, лояльный к ограничениям, да и готовая функция в R есть), сначала геокодируются адреса квартир и метро, функцией geocode из пакета ggmap. А расстояние определяется по формуле гаверсинуса, готовой функцией distHaversine из пакета geosphere.
Итоговое количество регрессоров составило 14 штук:
- Расстояние до метро
- Общая площадь
- Жилая площадь
- Площадь кухни
- Тип дома
- Наличие и типы лифтов
- Наличие и типы балконов
- Кол-во и типы санузлов
- Куда выходят окна
- Наличие телефона
- Тип продажи
- Первый этаж
- Последний этаж
- Пятиэтажка
Протестированные модели предиктивного анализа
1. МНК на все регрессоры
2. МНК с логарифмированием (разные варианты: логарифмированием цены и/или площадей и/или расстоянием до метро)
3. МНК с включением и исключением регрессоров
а) последовательным пошаговым исключениям регрессоров
б) алгоритмом прямого поиска
4. Модели со штрафами (для уменьшения влияния гетероскедастичности)
а) Лассо-регрессия (с 2 способами определения параметра фракционирования — минимизацией Cp-критерия Мэллоу и перекрестной проверкой)
б) Ридж-регрессия (с 3 способами нахождения параметра штрафа — методом HKB, LW и перекрестной проверкой)
5. Метод главных компонент
а) со всеми регрессорами
б) с пошаговым исключением регрессоров
6. Квантильная (медианная) регрессия (для уменьшения влияния гетероскедастичности)
7. Алгоритм случайного леса
Общее число протестированных моделей составило 18 штук.
В процессе подготовки моделей частично были использованы материалы:
Мастицкий С.Э., Шитиков В.К. (2014) Статистический анализ и визуализация данных с помощью R.
– Электронная книга, адрес доступа: r-analytics.blogspot.com
Критерии эффективности моделей
Результаты тестирования моделей
На различных случайных тестовых выборках (разные станции метро, разный тип квартир, прочие параметры), оценивались все вышеперечисленные модели по 3 критериям эффективности. И практически абсолютным (92%) победителем по всем 3 критериям, оказался алгоритм случайного леса. Также на разных выборках по некоторым критериям неплохие результаты показывала медианная регрессия, МНК с логарифмированием цены, полный МНК и иногда Ридж с Лассо. Результаты несколько удивительны, так как я полагал, что модели со штрафами могут оказаться лучше, чем полный МНК, но так было не всегда. Так что простая модель (МНК) может является лучшей альтернативой, чем более сложные. По причине того, что на разных выборках, по разным критериям места начиная со второго, занимали разные модели, а победителем оставался случайный лес, для дальнейшей работы решил использовать его.
Так как используется уже одна модель, то нет необходимости в явном указании дамми-переменных, поэтому возвращаемся к оригинальному фрейму, указав, что качественные переменные являются факторными, это упростит последующую трактовку на диаграмме, да и алгоритму будет проще (хотя ему по сути все равно). Для тестового моделирования использовалась функция randomForest (из одноименного пакета) со значениями по умолчанию, попробовав изменить основные параметры сложности деревьев nodesize, maxnodes, nPerm, определил, что чуть лучшую минимизацию ошибок прогноза при разных выборках достигается при изменении параметра nodesize (минимальное число узлов) в 1. Итак, модель выбрана.
Отображение на карте
Победитель определен (случайный лес), данная модель предсказывает «теоретические» цены для всех наблюдений с минимальными ошибками. Теперь можно посчитать абсолютную, относительную недооценку и результат вывести в виде отсортированной таблицы, но помимо табличного вида, хочется сразу информативности, поэтому выведем несколько лучших результатов сразу на карту. И для этого в R есть пакет googleVis предназначенный для интеграции с картографической системой Google (впрочем, есть пакет и для Leaflet). Я продолжил использовать также Google, так как полученные координаты от их геокодирования запрещено отображать на прочих картах. Отображение на карте осуществляется одной функцией gvisMap из пакета googleVis.
if ((err()!=" ")) return(NULL)
formap3<-formap()
formap3$desc<-paste0(row.names(formap3),
". №",
formap3$number,
" ",
formap3$address,
" недооценена на ",
format(-formap3$abs.discount, big.mark= " "),
" рублей (",
as.integer(formap3$otn.discount),
"%)")
gvisMap (formap3, «coord», «desc», options=list(
mapType='normal',
enableScrollWheel=TRUE,
showTip=TRUE))
})
Графический веб-интерфейс
Передавать все требуемые параметры через консоль медленно и неудобно, поэтому захотелось все сделать автоматизировано. И традиционно, для этого опять можно использовать R с фреймворками shiny, и shinydashboard, которые обладают достаточными элементами управления ввода-вывода.
dashboardHeader(title = «Mining Property v0.9» ),
dashboardSidebar(
sidebarMenu(
menuItem(«Source data», tabName = «Source»),
menuItem(«Summary», tabName=«Summary» ),
menuItem(«Raw data», tabName=«Raw» ),
menuItem(«Tidy data», tabName=«Tidy» ),
menuItem(«Predict data», tabName=«Predict» ),
menuItem(«Plots», tabName=«Plots» ),
menuItem(«Result map», tabName=«Map»)
)
),
dashboardBody(
tags$head(tags$style(HTML('.box{overflow: auto;}'))),
tabItems(
tabItem(«Source»,
box(width=12,
fluidRow(
column(width = 4,
selectInput(«Metro», «Метро», "", width='60%'),
# br(),
hr(),
#checkboxInput(«Kind.home0», «все», TRUE),
checkboxGroupInput(«Kind.home», «Тип дома»,c(
«панельный» = 1,
«сталинский» = 7,
«щитовой» = 8,
«кирпичный» = 2,
«монолитный» = 3,
«кирпично-монолитный»=4,
«блочный» = 5,
«дерев.»=6), selected=c(1,2,3,4,5,6,7,8)),
hr(),
sliderInput(«Etag», «Этаж», min=0, max=100, value=c(0, 100),step=1),
checkboxInput(«EtagP», «не последний»),
sliderInput(«Etagn», «В доме этажей», min=0, max=100, value=c(0, 100),step=1)
,
submitButton(«Анализировать», icon(«refresh»))
),
column(width = 4,
selectInput(«Rooms», «Комнат», c
("",
«1»="&room1=1",
«2»="&room2=1",
«3»="&room3=1"),width='45%'),
# br(),
hr(),
# br(),
selectInput(«Balcon», «Балкон»,
c(«можно без балкона»=«0»,
«только с балконом»="&minbalkon=1",
«только без балкона»="&minbalkon=-1"),
width='45%'),
br(),
hr(),
br(),
#br(),
sliderInput(«KitchenM», «Площадь кухни», min=0, max=25, value=c(0, 25),step=1),
sliderInput(«GilM», «Жил. площадь», min=0, max=100, value=c(0, 100),step=1),
sliderInput(«TotalM», «Общ. площадь», min=0, max=150, value=c(0, 150),step=1)
),
column(width = 4,
sliderInput(«Price», «Цена», min=0, max=50000000, value=c(0, 50000000),step=100000, sep=" "),
# hr(),
selectInput(«Deal», «Тип сделки»,
c(«любой»=«0»,
«свободн.»="&sost_type=1",
«альтернатива»="&sost_type=2"),
width='45%'),
br(),
hr(),
#br(),
# br(),
# br(),
radioButtons(«wc», «Санузел»,
c(«не важно» = "",
«раздельный» = "&minsu_r=1",
«совмещенный» = "&minsu_s=1")),
hr(),
selectInput(«Lift», «Лифтов (минимум)»,
c(«0»=0,
«1»="&minlift=1",
«2»="&minlift=2",
«3»="&minlift=3",
«4»="&minlift=4"
),
width='45%'),
hr(),
selectInput(«obs», «Отображать квартир на карте:»,c(1:10),selected=5, width=250),
textOutput(«flat»)
)
),
fluidRow( htmlOutput(«hyperf1»)),
fluidRow( textOutput(«testOutput»))
)
),
tabItem(«Raw», box(dataTableOutput(«Raw» ),width=12,height=600)),
tabItem(«Summary», box(verbatimTextOutput(«Summary» ),width=12,height=600)),
tabItem(«Tidy», box(dataTableOutput(«Tidy» ),width=12, height=600)),
tabItem(«Predict», box(dataTableOutput(«Predict» ),width=12, height=600)),
tabItem(«Plots», box(width=12, plotOutput(«RFplot», height=275),plotOutput(«r2», height=275))),
tabItem(«Map», box(width=12,htmlOutput(«view»), DT::dataTableOutput(«formap2»), height=600))
)
)
)
Результатом всего этого становится удобное приложение с графическим интерфейсом, с фактически двумя (остальные пункты для контроля) главными пунктами бокового меню – первым и последним. В первом (Source data) пункте бокового меню (рис. 1), задаются все требуемые параметры (аналогично cian) по поиску и оценки квартир.
Рис.1 Окно выбранного меню Source data
В остальных пунктах бокового меню выводится:
- сводный отчет (Summary) по регрессорам
- таблицы данных (сырая (Raw data) – исходная после парсинга, опрятная (Tidy data) – после приведения параметров в опрятный вид и корректировки параметров и добавлением геолокации, и итоговая (Predict data) таблица с предсказанными значениями цен)
- три диаграммы (Plots) (рис. 2)– точности модели и важности регрессоров в алгоритме случайного леса (почти всегда все регрессоры являются важными) и диаграмма рассеяния исходных и предсказанных цен.
Рис.2 Окно выбранного меню Plots
Ну а в последнем пункте (Result map) (рис. 3) отображается то, ради чего все и затевалось, карта с выбранными лучшими результатами и приводится таблица с рассчитанной предсказанной ценой и основными характеристиками квартир.
Рис.3 Окно выбранного меню Result map
Также в этой таблице сразу имеется ссылка (*) для перехода на данное объявление. Данную интеграцию (включение элементов JS в таблицу) позволяет сделать пакет DT.
Заключение
Резюмируя все вышеизложенное, как же все это работает:
- На первой страницы с помощью элементов управления задается исходный запрос
- На основе выбора данных элементов ввода, формируется строка запроса (также она указывается как гиперссылка для проверки)
- Данная строка с указанием страницы передается в API import.io (в процессе создания всего этого, cian начал менять интерфейс вывода, благодаря import.io переобучил я экстрактор буквально в течение 5 минут)
- Обрабатывается получаемый JSON от API
- Перебираются все страницы (в процессе работы отображается строка состояния по процессам)
- Таблицы склеиваются, проверяются (исключаются некорректные значения, заменяются пропущенные значения), приводятся в единообразный вид, пригодный для анализа
- Проводится геокодирование адресов и определение расстояния
- Строится модель по алгоритму случайного леса
- Определяются предсказанные цены, абсолютные, относительные отклонения
- На карте и в таблице под ней отображаются лучшие результаты (кол-во выводимых квартир указывается на первой странице)
Вся работа приложения (от начала запроса до отображения на карте) выполняется менее чем за минуту (большая часть времени уходит на геолокацию, ограничение Google для бытового использования).
Данной публикацией хотелось показать, как для простых бытовых нужд, в рамках одного приложения удалось решить много небольших, но принципиально разных интересных подзадач:
- краулинг
- парсинг
- интеграция со сторонним API
- обработка JSON
- геокодирование
- работа с различными моделями регрессий
- оценка их эффективности разными способами
- геолокация
- отображение на карте
Ко всему прочему все это реализовано в удобном графическом приложении, которое может быть как локальным, так и размещенном в сети, и все это сделано на одном R (не считая import.io), с минимумом строк кода с простым и изящным синтаксисом. Конечно, что-то не учитывается, например, дом рядом с шоссе или состояние квартиры (так как этого в объявлениях нет), но итоговый, отранжированный список вариантов, сразу отображаемых на карте и со ссылкой на само исходное объявление, значительно облегчают выбор квартир, ну и плюс ко всему узнал много нового в R.
Комментарии (34)
berezuev
10.08.2015 12:27+2А import.io по ходу дела слег под хабраэффектом…
Собственно, если от него в итоге отказаться (стать полностью автономными). то получился бы неплохой сервисatikhonov
10.08.2015 12:32Это вряд ли, ссылки то нет, к тому же import.io достаточно редко отваливается (через несколько секунд обычно снова работает).
Да, если отвязаться от import.io может было бы и лучше,
только сложнее будет модифицировать при изменениях дизайна.
igor_suhorukov
10.08.2015 12:48+1Лет 5 назад писал парсер для семьи друзей, когда они квартиру искали. У вас прямо чувствуется интерес к R))))
Dreamastiy
10.08.2015 12:58+4Отличная статья, жалко, что без подробностей (на GitHub например).
Как организовали работу Shiny под Win — виртуалка, запуск прямо из RStudio или у Shiny Server наконец-то появилась нативная поддержка Windows?atikhonov
10.08.2015 13:29Пока обкатываю — запуск из RStudio,
когда все готово, то в облако shinyapps.io (для себя) и на сервер linux (по работе)
P.S. Какие именно подробности интересуют?Dreamastiy
10.08.2015 13:42+1Мне лично были бы интересны подробности следующих частей:
1. Отображение на карте,
2. Графический веб-интерфейс,
3. Определение лямбы в Lasso и Ridge
Кстати, если Ridge и Lasso показывают себя плохо, имеет смысл попробовать их суперпозицию, которая в некоторых случаях ведет себя лучше.atikhonov
10.08.2015 14:01Обновил пост, добавил код отображения на карте, клиентского интерфейса и детализировал способы определения лямб.
Dreamastiy
10.08.2015 16:45+1Спасибо! А табличку метод регрессии — cost функция — результат можете выложить?
YgReEk
10.08.2015 13:28+2Для проверки качества объявлений есть проект sobnik, которые поддерживает в т.ч. и Циан. Разработчики открыты к диалогу, может имеет смысл объединить усилия и создать плагин для браузера, сортирующий таблицу поиска на циане или ином сайте по оптимальности? Аудитория у собника есть, опыт написания плагинов тоже, у вас отличная модель и сервис… Прямо всё готово для стартапа :)
arelay
10.08.2015 17:44+5Круто!
Наверно, выскажу желание многих:
Если не планируется как-то монетизировать, либо пускать проект в море — откройте
GitHub репозиторий с кодом проекта. Будем вам благодарны!
VenomBlood
11.08.2015 06:48Общее число протестированных моделей составило 18 штук.
Вспомнилась одна цитата:
If you torture the data long enough, it will confess to anything.
И дальше только больше убедился в этом:
определил, что чуть лучшую минимизацию ошибок прогноза при разных выборках достигается при изменении параметра nodesize (минимальное число узлов) в 1.
walker
11.08.2015 09:10+2А ровно 3 года назад это не вы написали отличную статью про R — Как я покупал квартиру?
DjOnline
11.08.2015 11:03Так чем дело закончилось? Покупкой квартиры на 30% дешевле рынка? Во всех новостях сейчас пишут что и так любой продавец готов скинуть минимум 10%, а то и 20% в условиях кризиса.
Кроме того на скриншоте вижу что нашлись в основном квартиры на первом и последнем этажах с якобы скидкой 6%, но это на самом деле не скидка, а естественный дисконт за первый и последний этаж, и его надо учитывать в формулах.atikhonov
11.08.2015 13:06А алгоритмах это уже все учитывается, эти квартиры должны стоить больше, относительно таких же
DmitriyH
11.08.2015 14:38-1Во всех новостях сейчас пишут что и так любой продавец готов скинуть минимум 10%, а то и 20% в условиях кризиса.
Не читайте советских газет перед обедом.
Ни мне, ни моим знакомым не известно ни одного случая, чтобы продавец был готов скинуть 10% стоимости от объявления, если квартира юридически чистая и это не объявление от перекупов. Даже если где-то написано «торг уместен», то в лучшем случае уступят 30-50 тыс.
ice_dog_as
11.08.2015 11:34-3А почему не обратили внимание на agenton.ru — 3000р за месяц с отсеянной базой)
atikhonov
11.08.2015 13:08Посмотрел на сайт, что-то никакой конкретной информации нет,
в каком виде объявления, можно ли их экспортировать и т.д.
В их видео тоже нет информации.
steamoor
11.08.2015 16:45+1http://habrahabr.ru/post/242085/ другая публикация на эту тему, интересный подход, но все по стандартному алгоритму…
Fedcomp
11.08.2015 23:45+1На самом деле уже была статья по покупке (или снятию) квартиры на том же R. И не только на нем, в общем на хабре уже две-три статьи подобного рода.
mstsvetk
05.09.2015 20:47Так купили-то квартиру?
atikhonov
06.09.2015 18:56Пока нет, сейчас отдельный локальный скрипт каждый день собирает статистику по интересующим меня вариантам, результаты интересные — длительная экспозиция, хорошее снижение цен, так что пока выжидаю
mstsvetk
06.09.2015 19:49да, прямой датамайнинг — это очень правильно.
«Верить, в наше время, нельзя никому. Порой даже себе. мне — можно.»(с) Мюллер.
но ваш вывод совпадает индексами irn.ru — даже рублевый индекс Московского квадрата загибается вниз.
Svyatov
Прикольно, но с кодом было бы еще лучше…
atikhonov
Какая именно подзадача интересует?
Могу обновить пост, выложить необходимые части
and7ey
Выложите, пожалуйста, код всех моделей — только изучаю R, было бы интересно посмотреть какие функции используются, как данные передаются и как оценивается результат.