Исследование объявлений о продаже квартир

Выборка исследования: архив объявлений о продаже квартир в Санкт-Петербурге за несколько лет - данные сервиса Яндекс.Недвижимость.

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

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

In [1]:
# Необходимые библиотеки для исследования 

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

%matplotlib inline


# Укажем удобный формат вывода для чисел с плавающей точкой

pd.options.display.float_format = '{:.3f}'.format
In [2]:
# Загрузка данных

data = pd.read_csv('real_estate_data.csv', sep='\t')
In [4]:
# Всего набор данных состоит из 23699 строк и 24 столбцов

data.shape
Out[4]:
(23699, 22)
In [5]:
# Изучем типы данных

data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 23699 entries, 0 to 23698
Data columns (total 22 columns):
total_images            23699 non-null int64
last_price              23699 non-null float64
total_area              23699 non-null float64
first_day_exposition    23699 non-null object
rooms                   23699 non-null int64
ceiling_height          14504 non-null float64
floors_total            23613 non-null float64
living_area             21796 non-null float64
floor                   23699 non-null int64
is_apartment            2775 non-null object
studio                  23699 non-null bool
open_plan               23699 non-null bool
kitchen_area            21421 non-null float64
balcony                 12180 non-null float64
locality_name           23650 non-null object
airports_nearest        18157 non-null float64
cityCenters_nearest     18180 non-null float64
parks_around3000        18181 non-null float64
parks_nearest           8079 non-null float64
ponds_around3000        18181 non-null float64
ponds_nearest           9110 non-null float64
days_exposition         20518 non-null float64
dtypes: bool(2), float64(14), int64(3), object(3)
memory usage: 3.7+ MB
In [17]:
# Проверка распределения ключевого показателя квартиры - цены - перед удалением строк с пустыми значениями 
# в столбцах с названием населенного пункта и общим кол-вом этажей в доме

ax = data[data['locality_name'].isnull()].plot(kind='hist', y='last_price', histtype='step', range=(0, 45000000), bins=50, label='delete_locality_name')

data[data['floors_total'].isnull()].plot(kind='hist', y='last_price', histtype='step', range=(0, 45000000), bins=50, ax=ax, label='delete_floors_total')

data.plot(kind='hist', y='last_price', histtype='step', range=(0, 45000000), bins=50, label='Стоимость квартир')

plt.title('Распределение цены квартир'+ "\n")
plt.xlabel('Цена')
plt.ylabel('Частотность')


plt.show()
In [18]:
# Просматриваем сводные статистические данные распределния стоимости кварт в удаляемых строках с пропуском названий населенного пункта

data[data['locality_name'].isnull()]['last_price'].describe()
Out[18]:
count         39.000
mean     6673978.000
std      4557874.365
min      1750000.000
25%      4225000.000
50%      5100000.000
75%      7400000.000
max     24000000.000
Name: last_price, dtype: float64
In [19]:
# Просматриваем сводные статистические данные распределния стоимости кварт в удаляемых строках с пропускам в этажнсти здания

data[data['floors_total'].isnull()]['last_price'].describe()
Out[19]:
count         38.000
mean     8346103.211
std      6945767.789
min      2895000.000
25%      4514659.250
50%      5861077.500
75%     10169414.750
max     40348248.000
Name: last_price, dtype: float64

Удаление строчек с пустыми значениями в столбцах floors_total и locality_name сильно не повлияют на общее распределение ключевого показателя исследования.

Исследовательский анализ данных

Каковы типичные параметры продаваемых квартир? Сколько обычно длится процесс продажи?

Наиболее типичные параметры квартиры: цена, общая площадь квартиры и количество комнат.

In [39]:
# Отбираем целевые характеристики квартиры

typical_flat = data[['last_price', 'rooms', 'total_area']]
In [40]:
# Описательная статистика целевых характеристик

typical_flat.describe()
Out[40]:
last_price rooms total_area
count 20511.000 20511.000 20511.000
mean 6443888.805 2.107 60.525
std 9510134.225 1.067 34.577
min 430000.000 1.000 14.000
25% 3500000.000 1.000 40.500
50% 4680000.000 2.000 52.400
75% 6750000.000 3.000 70.000
max 420000000.000 19.000 900.000
In [41]:
# Распределение характеристик


typical_flat.hist(figsize=(10,10), bins=30)

plt.title('Распределение типичных характеристик квартир'+ "\n")
plt.xlabel('Цена')
plt.ylabel('Частотность')


plt.show()

Целевые характеристики содержат экстремальные значения. Это видно по расчетам описательных статистик, так и по визуализации распределения.

Например, стоимость квартиры колеблется от 450 тысяч и до 420 миллионов. Подобная картина в количестве комнат: самая многоквартирная квартира включает 19 комнат.

Исследуем цену квартир

In [48]:
data['last_price'].describe()
Out[48]:
count       20511.000
mean      6443888.805
std       9510134.225
min        430000.000
25%       3500000.000
50%       4680000.000
75%       6750000.000
max     420000000.000
Name: last_price, dtype: float64
In [49]:
data.plot(kind='box', y='last_price')

plt.show()
In [50]:
data.plot(kind='box', y='last_price', ylim=(0, 12000000))

plt.show()

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

In [51]:
# Вычисление границы до выбросов

data['last_price'].describe()['75%'] + 1.5*(data['last_price'].describe()['75%']-data['last_price'].describe()['25%'])
Out[51]:
11625000.0
In [52]:
data['last_price'].hist(range=(0, 11875000))

plt.title('Распределение стоимости квартиры до 11,875 млн.'+ "\n")
plt.xlabel('Стоимость')
plt.ylabel('Частотность')

plt.show()
In [53]:
data['last_price'].hist(range=(11875000, 100000000))

plt.title('Распределение стоимости квартиры превыщающую 11,875 тыс.'+ "\n")
plt.xlabel('Цена')
plt.ylabel('Частотность')

plt.show()
In [54]:
print('Количество квартир, стоимость которых превышает 50 млн. - ', len(data[data['last_price'] > 50000000]), 'квартиры')
Количество квартир, стоимость которых превышает 50 млн. -  102 квартиры
In [55]:
# Описательные статистики квартир до 11,875 тыс.

data.query('last_price <= 11875000')['last_price'].describe()
Out[55]:
count      18800.000
mean     4878410.083
std      2193428.325
min       430000.000
25%      3400000.000
50%      4490000.000
75%      6000000.000
max     11866860.000
Name: last_price, dtype: float64
In [56]:
# Распределение квартир до 3,4 млн. - 1 квартиль

data['last_price'].hist(range=(0, 3400000))

plt.title('Распределение стоимости квартиры до 3,4 млн.'+ "\n")
plt.xlabel('Стоимость')
plt.ylabel('Частотность')


plt.show()
In [64]:
# Проверим, на сколько удаление дорогостоящих квартир скажется на распредленении цены

ax = df_good.plot(kind='hist', y='last_price', histtype='step', range=(0, 35000000), bins=50, label='Good data')

data.plot(kind='hist', y='last_price', histtype='step', range=(0, 35000000), bins=50, ax=ax, label='Data', linestyle='dashed')

plt.show()
In [67]:
# Визуализация количественных характеристик

df_good.select_dtypes(include=np.number).iloc[:, :24].hist(figsize=(15,15), bins=30)

plt.show()
In [68]:
# Описательные характеристики

df_good.select_dtypes(include=np.number).iloc[:, :24].describe()
Out[68]:
total_images last_price total_area rooms ceiling_height floors_total living_area floor kitchen_area balcony ... parks_nearest ponds_around3000 ponds_nearest days_exposition price_per_meter weekday month year ratio_of_living_area ratio_of_kitchen_area
count 18800.000 18800.000 18800.000 18800.000 11859.000 18800.000 18800.000 18800.000 18800.000 18800.000 ... 5953.000 14249.000 6742.000 16426.000 18800.000 18800.000 18800.000 18800.000 18800.000 18800.000
mean 9.901 4878410.083 53.779 1.969 2.722 10.564 30.998 5.786 9.377 0.619 ... 484.579 0.699 533.338 178.017 91592.592 2.555 6.426 2017.370 0.566 0.185
std 5.534 2193428.325 18.842 0.897 0.959 6.512 13.400 4.794 3.366 0.953 ... 318.712 0.890 277.354 213.985 28727.577 1.786 3.465 1.026 0.098 0.063
min 0.000 430000.000 14.000 1.000 1.000 1.000 8.300 1.000 1.300 0.000 ... 1.000 0.000 16.000 1.000 9615.385 0.000 1.000 2014.000 0.141 0.044
25% 6.000 3400000.000 39.500 1.000 2.500 5.000 18.500 2.000 7.000 0.000 ... 289.000 0.000 315.000 43.000 74631.716 1.000 3.000 2017.000 0.500 0.135
50% 10.000 4490000.000 50.000 2.000 2.600 9.000 29.700 4.000 9.000 0.000 ... 456.000 0.000 522.000 97.000 92259.300 3.000 6.000 2017.000 0.568 0.173
75% 14.000 6000000.000 64.000 3.000 2.750 15.000 39.900 8.000 11.000 1.000 ... 612.000 1.000 746.000 228.000 108490.566 4.000 10.000 2018.000 0.636 0.230
max 50.000 11866860.000 190.000 6.000 32.000 60.000 127.000 33.000 43.400 5.000 ... 3190.000 3.000 1344.000 1580.000 262711.864 6.000 12.000 2019.000 0.976 0.591

8 rows × 23 columns

Исследуем цену за квадратный метр квартиры

In [79]:
# Описательные статистики цены за квадратный метр

df_good['price_per_meter'].describe()
Out[79]:
count    18527.000
mean     91404.477
std      28646.978
min       9615.385
25%      74509.804
50%      92124.814
75%     108333.333
max     262711.864
Name: price_per_meter, dtype: float64
In [80]:
df_good.plot(kind='box', y='price_per_meter')

plt.show()

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

In [81]:
df_good['price_per_meter'].hist(bins=20)

plt.title('Распределение стоимости квартир за квадратный метр'+ "\n")
plt.xlabel('Цена за квадратный метр')
plt.ylabel('Частотность')


plt.show()

Распределение цены за квадратный метр похоже на нормальное распределение.

In [82]:
# Проверка на сколько удаление экстремальных значений повлияет на распределение цены за квадратный метр квартиры

ax = df_good.plot(kind='hist', y='price_per_meter', histtype='step', bins=50, label='Old data')

df_good.query('price_per_meter < 170000').plot(kind='hist', y='price_per_meter', histtype='step', bins=50, ax=ax, label='New Data', linestyle='dashed')

plt.show()
In [83]:
df_good.query('price_per_meter < 20000').describe()
Out[83]:
total_images last_price total_area rooms ceiling_height floors_total living_area floor kitchen_area balcony ... parks_nearest ponds_around3000 ponds_nearest days_exposition price_per_meter weekday month year ratio_of_living_area ratio_of_kitchen_area
count 134.000 134.000 134.000 134.000 51.000 134.000 134.000 134.000 134.000 134.000 ... 2.000 4.000 2.000 102.000 134.000 134.000 134.000 134.000 134.000 134.000
mean 8.769 865447.761 54.498 2.209 2.626 4.216 32.599 2.634 7.597 0.358 ... 643.500 1.250 410.000 168.824 16063.391 2.948 6.657 2017.575 0.609 0.151
std 4.514 355037.636 22.017 0.859 0.179 1.596 12.131 1.563 1.923 0.642 ... 415.072 1.500 343.654 175.954 2627.360 1.812 3.286 0.976 0.115 0.049
min 0.000 430000.000 27.600 1.000 2.500 1.000 12.500 1.000 5.000 0.000 ... 350.000 0.000 167.000 3.000 9615.385 0.000 1.000 2015.000 0.141 0.068
25% 6.000 650000.000 42.250 2.000 2.500 3.000 25.625 1.000 6.000 0.000 ... 496.750 0.000 288.500 45.000 14465.025 1.000 4.000 2017.000 0.556 0.115
50% 8.500 799500.000 52.000 2.000 2.550 5.000 31.000 2.000 7.200 0.000 ... 643.500 1.000 410.000 108.500 16170.448 3.000 7.000 2018.000 0.620 0.143
75% 11.000 990000.000 61.875 3.000 2.700 5.000 40.975 4.000 8.500 1.000 ... 790.250 2.250 531.500 229.750 18468.658 4.000 9.750 2018.000 0.679 0.172
max 20.000 3200000.000 190.000 5.000 3.000 9.000 90.700 9.000 15.300 2.000 ... 937.000 3.000 653.000 847.000 19969.512 6.000 12.000 2019.000 0.859 0.438

8 rows × 23 columns

In [84]:
# Создаем дасатет по областям, где стоимость за квадратный метр меньше 20 тысяч

data_price_per_meter = (
    pd.DataFrame(df_good.query('price_per_meter < 20000').groupby('locality_name')['price_per_meter'].count())
    .join(df_good.groupby('locality_name')['price_per_meter'].count().sort_values(), lsuffix='_<20000', rsuffix='_all')
)
    
In [85]:
# Доля дешевых квартир по городам

data_price_per_meter['rate'] = (data_price_per_meter['price_per_meter_<20000']/
                                data_price_per_meter['price_per_meter_all'])*100
In [88]:
df_good.query('price_per_meter > 170000')['cityCenters_nearest'].describe()
Out[88]:
count     139.000
mean     7059.942
std      3651.111
min      1162.000
25%      4599.000
50%      5826.000
75%      9048.000
max     22801.000
Name: cityCenters_nearest, dtype: float64
In [89]:
# Удаляем выбросы

df_good = df_good.drop(df_good.query('price_per_meter > 170000').index).reset_index(drop=True)

print('Количество квартир в наборе данных:', len(df_good))
Количество квартир в наборе данных: 18387

Какие факторы больше всего влияют на стоимость квартиры?

Отдельно изучите, зависит ли цена квадратного метра от числа комнат, этажа (первого или последнего), удалённости от центра и даты размещения: дня недели, месяца и года.

In [114]:
# Рассчитывает корреляцию для стоимости квартир

df_good.corr()['last_price'].sort_values()
Out[114]:
cityCenters_nearest     -0.328
ratio_of_kitchen_area   -0.157
ponds_nearest           -0.056
airports_nearest        -0.031
weekday                 -0.010
month                    0.002
is_apartment             0.006
year                     0.020
parks_nearest            0.040
ratio_of_living_area     0.047
days_exposition          0.049
balcony                  0.078
parks_around3000         0.079
ponds_around3000         0.118
total_images             0.161
floor                    0.178
floors_total             0.259
rooms                    0.432
ceiling_height           0.433
kitchen_area             0.501
living_area              0.564
price_per_meter          0.648
total_area               0.696
last_price               1.000
studio                     nan
open_plan                  nan
check_area                 nan
Name: last_price, dtype: float64

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

In [115]:
corr_price = df_good[['last_price', 'rooms', 'ceiling_height', 'kitchen_area', 'living_area', 'total_area', 'price_per_meter']]

pd.plotting.scatter_matrix(corr_price, figsize=(15, 15))
plt.show()

Промежуточные выводы

1) Зависимость между стоимостью квартиры и ценой за кв.м., а также между ценой и общей площади квартиры положительная и достаточно сильная: с увеличением стоимости квартиры, увеличивается стоимость за квадратный метр и общая площадь помещения.

2) Зависимость между ценой и жилой площадью можно визуально можно разделить на 4 кластера:

  • жилая площадь до 20 кв.м.
  • жилая площадь от 20 до 40 кв.м.
  • жилая площадь от 40 до 50 кв.м.
  • жилая площадь более 50 кв.м.

Устойчивая связь наблюдается у квартир стоимость 4,5 - 8 млн. и жилой площадью до 50 квадратных метров. Интересно, что квартиры с жилой площади более 50 кв.м. неравномерно связаны со стоимостью квартиры: цена квартиры может быть как до 5 млн., так и более 10 млн. с жилой площадью более 50 кв.м.

3) Устойчивая связь наблюдается между ценой и квартир с кухней до 15 кв.м. Затем с увеличением кухни стоимость квартиры сильно вариьруется.

4) Положительные и линейные связи видны между ценой и высотой потолка. Устойчивую связь можно наблюдать с высотой потолков от 2.5 и до 3.0 метров.

5) Присутствует четкая зависимость между ценой площади и одно-, двух- и трехкомнатных квартир. Стоимость многокомнатных квартир малопредсказуема - прямолинейной зависимости нет.

Сегментирование квартир в центре и вне центра

Выделите сегменты типичных квартир в центре (по удалённости, числу комнат и площади) и вне центра. Сравните корреляцию основных факторов с ценой по всем предложениям и объявлениям в вашей выборке.

In [129]:
df_good.plot(kind='density', y='cityCenters_nearest',label='Удаленность от центра', figsize=(14,5))
plt.xlim((0,df_good['cityCenters_nearest'].max() + 5000))

plt.title('Плотность распределения удаленности от центра')
plt.grid(True)
plt.xlabel('Удаленность от центра, м.')

plt.show()

По распределению удаленности от центра видно, что самые "центровые" квартиры находятся примерно в 8 км. удаленности от центра.

Наибольшее количество квартир сосредоточенно в пределах 10-17 км. удаленности от центра.

In [130]:
# Распределение квартир по населенным пунктам

ax = plt.gca()

df_good.query('locality_name == "Санкт-Петербург"').plot(kind='density', y='cityCenters_nearest', ax=ax, label='Санкт-Петербург', figsize=(14,5))
df_good.query('locality_name != "Санкт-Петербург"').plot(kind='density', y='cityCenters_nearest', ax=ax, label='Остальные населенные пункты', figsize=(14,5))

plt.title('Плотность распределения Санкт-Петербурга и остальных населенных пунктов по удаленности от центра')
plt.xlim((0,df_good['cityCenters_nearest'].max() + 5000))
plt.grid(True)
plt.xlabel('Удаленность от центра, м.')

plt.show()

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

In [131]:
# Проверка количества квартир в каждой группе

print('до 8,5 км.:', len(df_good.query('cityCenters_nearest < 8500')))
print('между 8,5-20 км.:', len(df_good.query('8500 <= cityCenters_nearest < 20000')))
print('более 20 км.:', len(df_good.query('20000 <= cityCenters_nearest ')))
до 8,5 км.: 1917
между 8,5-20 км.: 9400
более 20 км.: 2189
In [132]:
# Функция для категоризации

def category_distance(dis):
    if dis < 8500:
        return 'до 8,5 км'
    elif dis < 20000 and dis >= 8500:
        return 'между 8,5-20 км'
    elif dis >= 20000:
        return 'более 20 км'
In [133]:
# Категоризация

df_good['category'] = df_good['cityCenters_nearest'].apply(category_distance)
In [134]:
# Проверка категоризации

df_good['category'].value_counts()
Out[134]:
между 8,5-20 км    9400
более 20 км        2189
до 8,5 км          1917
Name: category, dtype: int64
In [135]:
# Корреляция для квартир по категориям удаленности от центра

data_corr = (
    pd.DataFrame(df_good.query('category == "до 8,5 км"').corr()['last_price'])
    .join(pd.DataFrame(df_good.query('category == "между 8,5-20 км"').corr()['last_price']),
    lsuffix='_до 8,5 км', rsuffix='_между 8,5-20 км')
)

data_corr = data_corr.join(pd.DataFrame(df_good.query('category == "более 20 км"').corr()['last_price']))
data_corr.columns = ['до 8,5 км', 'между 8,5-20 км', 'более 20 км']
In [136]:
data_corr.sort_values(by='более 20 км')
Out[136]:
до 8,5 км между 8,5-20 км более 20 км
ratio_of_kitchen_area -0.171 -0.220 -0.207
cityCenters_nearest -0.170 -0.097 -0.146
airports_nearest -0.024 0.060 -0.066
ponds_nearest -0.043 -0.011 -0.047
weekday 0.024 -0.012 -0.038
month -0.013 -0.008 -0.008
parks_around3000 0.039 0.023 -0.002
parks_nearest 0.068 0.046 0.003
year -0.001 0.083 0.036
is_apartment -0.024 -0.003 0.058
days_exposition 0.160 0.044 0.083
ponds_around3000 0.095 0.052 0.105
balcony 0.085 0.155 0.118
ratio_of_living_area 0.016 0.072 0.136
floors_total 0.174 0.261 0.148
floor 0.180 0.160 0.151
total_images 0.088 0.201 0.206
ceiling_height 0.315 0.380 0.395
price_per_meter 0.350 0.389 0.430
kitchen_area 0.496 0.549 0.531
rooms 0.469 0.495 0.569
living_area 0.600 0.655 0.681
total_area 0.755 0.809 0.790
last_price 1.000 1.000 1.000
studio nan nan nan
open_plan nan nan nan
check_area nan nan nan

Вывод

Во всех категориях сильная положительная корреляция между ценой и общей площадью квартиры - выше 7,5. Такая высокая корреляция объясняется третьим фактором - жилой площадью.

Площадь кухни в самых "центральных" квартирах влияет на цену меньше, чем в других группах. Больше всего цена зависит от площади кухни в квартирах, которые расположены в среднем сегменте по удаленности от центра.

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