title поиск квартиры часть 1

Введение

Данный пост является одним из серии описывающей обработку данных с использованием Pandas,Numpy и иных средств визуализации/анализа данных.

Disclaimer(отказ от ответственности)

За основу взяты данные о ценах на недвижимость в городе Екатеринбурге. Данные были получены из открытых источников.
Одновременно с этим данные НЕ являются:

  • публичной офертой что указывает цену на тот или иной объект недвижимости.
  • информацией по которой можно судить о состоянии рынка недвижимости.
  • информацией которую можно использовать для извлечения выгоды или планирования.
  • информацией которую можно использовать для принятия каких либо решения или получение каких либо выводов на её основе.

Любые совпадения случайны.

Pre-requierments

Для работы с используемым набором данных(здесь и далее - датасет(dataset)) на данном этапе нам потребуется стандартная библиотека языка Python, широко известные Pandas
и Numpy , a также библиотека GeoPy необходимый функционал из которой вы можете реализовать сами

1
2
3
4
import json
import pandas as pd
import numpy as np
import geopy.distance

Описание данных

1
2
3
4
5
6
7
8
9
#Get our data from a json-based file
with open('flats.json', 'r') as fout:
flats = json.load(fout)
#Change a data representation for serving data
df = pd.DataFrame(flats)
#Show our data in transformed view
df.head(4).T

Данная таблица развернута(транспонирована), поскольку экран не вмещает все признаки записи в используемом датасете.

0 1 2 3
cost 4800000 1900000 3200000 2990000
district Виз Заречный Втузгородок Ю-з
exterior False True False True
house_number 58 26 2 31/4
kitchen_area 9 7 6 8
latitude 56.8362 56.7944 56.8579 56
live_area 30 19 29 28
live_floor 6 4 2 1
longitude 60.5592 60.7745 60.6556 60
quickly NaN NaN NaN NaN
repair капитальный требуется косметический NaN
rooms 2 1 2 2
street Татищева Белоярская Студенческая Серафимы дерябиной
total_area 50 33 46 50
total_floor 10 5 4 5
type_house Улучшенной Улучшенной Полнометражка Улучшенной
walls Монолит Кирпич Кирпич Кирпич
wc Раздельный Совмещенный Совмещенный Раздельный
year 2010 1984 1959 1987

В свою очередь каждый атрибут у сущности в нашей выборке отражает тот или иной признак присущий конкретной записи.
Признаки:

  • cost -> Цена за квартиру
  • district -> Микрорайон в котором расположена квартира
  • street -> Улица которой расположена квартира
  • house_number -> Порядковый номер дома на улице
  • rooms -> Количество комнат
  • exterior -> Экстерьер квартиры(балконы/[полу-]лоджии)
  • total_area -> Общая площадь квартиры
  • kitchen_area -> Площадь кухни
  • live_area -> Жилая площадь
  • type_house -> Тип планировки дома
  • walls -> Материал стен
  • total_floor -> Число этажей в доме
  • live_floor -> Этаж на котором расположена квартира
  • repair -> Наличие ремонта в квартире
  • wc -> Тип туалета
  • year -> Год постройки
  • latitude -> Географические координаты: широта
  • longitude -> Географические координаты: долгота
  • quickly -> Признак что владелец хочет продать квартиру как можно быстрее

Выделение целевой выборки

Решено было не смешивать квартиры с разным количеством комнат, для создания более точной модели. За основу были взяты однокомнатные квартиры как наиболее “ходовые”. Также были отделены записи с “дефолтными” координатами.

1
2
3
#Base filtering
df = df[df.rooms == 1]
df = df[df.longitude > 60]

Основные проблемы анализа данных

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

  • Начальная обработка данных.
    Она занимает основное время, это подготовка всех данных для построенния будущей модели. Их очистка, удаление ненужных признаков и создание новых.
  • Обработка категориальных признаков.
    Большое количество библиотек анализа данных хорошо работает с численными признаками, но не может использовать данные в формате которые не могут быть представлены в виде числа.

В данном посте будут затронуты обе. Начнем с проблемы категориальных переменных(например строки что отражают тот или иной признак).
Для избежания данной проблемы можно использовать подходы с Dummy или OneHot кодированием. Но данные подходы “раздувают” нашу выборку и не всегда являются подходящими. Вместе с тем для анализа необходимо иметь представление(пускай и начальное) о предметной области, что мы и используем в нашем решении.

Районы

Каждая из квартир расположена в районе, но не все районы одинаково полезны предпочтительно для проживания в них. Логично предположить что чем ближе к центру район, тем сильнее это влияет на цену квартиры.
Поэтому пусть центральный район будет представлен 0, а все иные, по мере отдаления его - будут находится в “поясах” что расположены вокруг центра. И чем дальше микрорайон находится от центра - тем больше число. Получается своеобразный “штраф” за отдаленность от центра города. Сам центральный микрорайон таких “штрафов” не имеет. Данный подход с штрафами/градациями будет применен и дальше.

1
2
3
4
5
6
7
8
9
10
11
12
#Table for district
districts = {'Центр': 0, 'Автовокзал': 1, 'Ботанический': 1,
'Виз': 1, 'Вокзальный': 1, 'Парковый': 1,
'Втузгородок': 1, 'Пионерский': 1, 'Заречный': 1, 'Ю-з': 1,
'Шарташский р-к': 2, 'Академический': 2, 'Уралмаш': 2,
'Жби': 2, 'Завокзальный': 2, 'Карасьеозерск': 2,
'Н.сортировка': 2, 'Сибирский': 2, 'Широкая речка': 2,
'Эльмаш': 2, 'Уктус': 2,
'С.сортировка': 3, 'Лечебный': 3, 'Синие камни': 2,
'Солнечный': 3, 'Унц': 3, 'Компрессорный': 3,
'Елизавет': 3, 'Химмаш': 3, 'Чермет': 3
}

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

1
2
3
#Convert district names to their rate
df = df[df.district.isin(districts.keys())]
df.district = df.district.map(districts)

Тип стен

Известно что дома из кирпича наиболее предпочтительны(звукоизоляция, чистота воздуха в квартире, прочие аспекты). Панельные дома же, что строились в период после распада СССР/в его последние годы - не отличаются хорошим качеством по общему мнению.
В данном случае мы можем использовать подход с предыдущего шага и составить карту соответствия “тип стен в доме”->”штраф/бонус к цене”, взяв за основу вариант с кирпичными стенами

1
2
3
4
5
6
7
8
9
10
#Table for walls
walls = {
'Панель': 0.4,
'Кирпич': 0,
'Монолит': 0.1,
'Газозолобетон': 0.1,
'Шлакоблок': 0.3,
'Блок': 0.2,
'Бетон': 0.2
}
1
2
3
#Convert walls names to their rate
df = df[df.walls.isin(walls.keys())]
df.walls = df.walls.map(walls)

Тип постройки/планировка

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#Table for house's type
type_houses = {
'Улучшенной': 0,
'Полногабаритная': 0,
'Полнометражка': 0,
'Спец. планировка': 0,
'Брежневка': -0.2,
'Пентагон': -0.1,
'Хрущевка': -0.3,
'Малосемейка': -0.4,
'Эконом': -0.4,
'Типовая': -0.1,
'Бетон': -0.2,
'Студия': -0.1,
'Новая': 0,
'Индивидуальная': 0
}
1
2
3
#Convert house types to their rate
df = df[df.type_house.isin(type_houses.keys())]
df.type_house = df.type_house.map(type_houses)

Ремонт,его наличие

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

1
2
3
4
5
6
#Table for repair's type
repairs = {
'требуется': -1,
'косметический': 0,
'капитальный': 1
}
1
2
3
#Convert repair to rate
df = df[df.repair.isin(repairs.keys())]
df.repair = df.repair.map(repairs)

Дополнительные атрибуты

С оставшимися атрибутами всё просто. Если они есть - они определенно влияют на цену в сторону уменьшения/или увеличения:

  • Наличие балкона/лоджии - это плюс к цене.
  • Если есть признак срочной продажи - продавец будет готов уменьшить -цену, что отразится на ней тоже
  • Квартиры с совмещенным сан. узлом стоят дешевле, нежели с раздельным.

Использовав эти утверждения - изменим представление данных

1
2
3
df.exterior = np.where(df.exterior, 1, 0)
df.quickly = np.where(df.quickly == True, 1, 0)
df.wc = np.where(df.wc == 'Совмещенный', 0, 1)

Теперь все важные признаки, что имели текстовый вид - представлены в числовом виде в соответствии той или иной логике

Custom-аттрибуты

Как было сказано ранее - датасет может быть расширен дополнительными(custom) признаками, которые создаются на основе уже существующих

Рaccтояние до центра города

Не для кого не секрет(да и упоминалось ранее), что квартиры в центре более дорогие чем на окраине.
Но как использовать данное утверждение более явно?
Можно попробовать измерить расстояние до от центра города, до каждой квартиры, и сохранить эту метрику. Есть вероятность что это достаточно грубый способ отразить “отдаленность” от центра, но тем не менее - имеет смысл попробовать, поскольку просто координаты широты и долготы - нам ничего не дадут.

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

1
2
3
4
5
6
7
lon = 60.6125
lat = 56.8575
df['distance'] = 0
#FIXME:Is there better way instead using for-cycle?
for i, row in df.iterrows():
distance = geopy.distance.vincenty((lon, lat), (row.longitude, row.latitude))
df.at[i, 'distance'] = distance.km

Этаж

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

1
2
df['last_floor'] = df.live_floor == df.total_floor
df['first_floor'] = df.live_floor == 1

Финальная обработка

1
df.head(5).T

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

0 2 5 16 17
cost 4800000 3200000 2500000 4670000 3199000
district 1 1 1 1 1
exterior 0 0 1 1 0
house_number 58 2 30 10 13/1
kitchen_area 9 6 6 6 5
latitude 56.8362 56.8579 56.8352 56.8435 56.8522
live_area 30 29 29 29 32
live_floor 6 2 2 5 1
longitude 60.5592 60.6556 60.5526 60.5737 60.6302
quickly 0 0 0 0 0
repair 1 0 0 0 1
rooms 2 2 2 2 2
street Татищева Студенческая Заводская Юмашева Советская
total_area 50 46 43 52 46
total_floor 10 4 5 10 5
type_house 0 0 -0.3 0 -0.2
walls -0.1 0 0 0 -0.4
wc 1 0 1 1 0
year 2010 1959 1960 1988 1969
distance 6 4 6 4 1
last_floor False False False False False
first_floor False False False False True

Удаление лишних столбцов/строчек

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

Пришло время удалить мусор.

1
2
3
4
5
useless_features = ['live_area', 'kitchen_area', 'street',
'house_number', 'rooms', 'longitude',
'latitude', 'live_floor', 'total_floor']
df.drop(columns=useless_features, inplace=True)
df.dropna(inplace=True)

Итоговый dataset

1
df.head(5).T
0 2 5 16 17
cost 4800000 3200000 2500000 4670000 3199000
district 1 1 1 1 1
exterior 0 0 1 1 0
quickly 0 0 0 0 0
repair 1 0 0 0 1
total_area 50 46 43 52 46
type_house 0 0 -0.3 0 -0.2
walls -0.1 0 0 0 -0.4
wc 1 0 1 1 0
year 2010 1959 1960 1988 1969
distance 6 4 6 4 1
last_floor False False False False False
first_floor False False False False True

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