Время прочтения: 8 мин.

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

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

Для разработки инструмента нами был выбран язык программирования Python с применением библиотеки «Pandas» и «Pyodbc» для выгрузки информации из баз данных. Данные библиотеки использовались для предварительной подготовки данных (объединения выгрузок, отбора нужных столбцов, подготовки выборок наборов данных по интересующим параметрам) и обработки интересующего массива данных.

import pandas as pd
import pyodbc

conn = pyodbc.connect(r'Driver={SQL Server};Server=IP\database;'+
                      								 				'Database= DatabaseName;Trusted_Connection=yes;'+\
                      'useFMTONLY=Yes;') 
cur = conn.cursor()
query_db = ‘SELECT * FROM table_name1’
query_dict = ‘SELECT name1, name2… FROM table_name2’
df = pd.read_sql(query_db, conn)
dictionary = pd.read_sql(query_dict, conn)

Учитывались следующие критерии: адрес объекта, координаты, площадь объекта, цена. Также объекты были разбиты на две категории:

  • Первая – это помещения, где происходило обслуживание клиентов, удаленность между такими объектами не должна превышать 500 м;
  • Вторая – это складские и архивные помещения, удаленность между такими объектами не должна превышать 10-30 км.

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

info.json
{
     "cluster": {

         "cluster_by_coordinates": {
         "_comment": "кластеризация по географическим координатам (долгота и широта), delta - максимально допустимое расстояние(км) между соседними объектами",
         "dictionary_column": "name_of_column_with_objects_type", 
         "delta, km": {
         "object_type_1": 0.5, 
         "object_type_2": 1, 
         "object_type_3": 30
                        }, 
                        "status": {
                                "delta, %": 10,
                                "0": "Criterion_0(from_DB)", 
                                "1": "Criterion_1(from_DB)"
                        }, 
                        "coordinates": {
         "Longitude(L)": "name_of_column_with_longitudes", 
         "Latitude(W)": " name_of_column_with_latitudes"
                        }, 
         "columns_for_report": ["column1", "column2", 												"column3"…]
                }, 
         "cluster_by_columns": {
         "_comment": "кластеризация по заданным столбцам",
         "columns": ["column_1", "column_2", "column_3"…]
                }
        }
}

С помощью данного кода считывается информация из файла info.json:

def readjson(injson):
    with codecs.open(injson, 'r', 'utf_8_sig') as f:
        info = (json.load(f))
    return info

def parsejson(info):
        info = info['cluster']
        cluster1 = info['cluster_by_coordinates']
        cluster2 = info['cluster_by_columns']
        dictionary_column = cluster1['dictionary_column']
        deltadict = cluster1['delta']
        status = cluster1['status']
        deltacrit = status['delta, %']
        crit0 = status['0']
        crit1 = status['1']
        coordinates = cluster1['coordinates']
        Len = coordinates['Longitude(L)']
        Wid = coordinates['Latitude(W)']
        columns_for_report = cluster1['columns_for_report']
   return [dictionary_column, deltadict, deltacrit, crit0, crit1, Len, Wid, columns_for_report]

Подготовка координат объектов для дальнейшего расчёта. Удаляются некорректные значения и отбираются объекты по типу назначения. Для дальнейшего удобства обработки формируются отдельные списки с координатами, индексами, статусами.

def dots_init(dictionary, database, nameON, dictionary_column, crit1, Len, Wid):
    cur_objects = dictionary[dictionary[dictionary_column]==nameON]
    correctind = [i for i in cur_objects.index if i in database.index]
    cur_objects = database.loc[correctind]
    log('Запущен процесс сбора значений долготы')
    X = cur_objects[Len]
    log('Запущен процесс сбора значений широты')
    Y = cur_objects[Wid]
    stat = {i:int(database[crit1][i]>0) 
             for i in cur_objects.index}
    dots = []
    strangearch = []
    status = []
    indexes = []
    log('Запущен процесс подготовки координат')
    for i,x,y in zip(X.index, X.values, Y.values):
        if (x>0 and y>0):
            dots.append([x,y])
            status.append(stat[i])
            indexes.append(i)
        else:
            strangearch.append([X[i],Y[i]])
    n = (len(dots))
    if n%10 == 1:
        ending = 'а'
    elif n%10 >= 2 and n%10 <= 9:
        ending = 'ы'
    else:
        ending = ''
    log('Подготовлено ' + str(n) + ' координат'+ending)
    return cur_objects, dots, status, indexes

Сортировка координат по широте:

def sortX(dots, indexes, status):
    log('Запущен процесс сортировки координат по широте')
    leng = len(dots)
    for i in range(leng-1):
        for j in range(i+1,leng):
            if dots[i][0]>dots[j][0]:
                dots[i],dots[j] = dots[j],dots[i]
                status[i], status[j] = status[j], status[i]
                indexes[i], indexes[j] = indexes[j], indexes[i]
    return dots, indexes, status

Сортировка координат по долготе:

def sortY(dots, indexes, status):
    log('Запущен процесс сортировки координат по долготе')
    leng = len(dots)
    for i in range(leng-1):
        for j in range(i+1,leng):
            if dots[i][0]==dots[j][0]:
                if dots[i][1]>dots[j][1]:
                    dots[i],dots[j] = dots[j],dots[i]
                    status[i], status[j] = status[j], status[i]
                    indexes[i], indexes[j] = indexes[j], indexes[i]
    return dots, indexes, status

Далее функция calc находит для каждого сдаваемого/арендуемого объекта ближайшие арендуемые/сдаваемые объекты на заданном расстоянии и сравнивает по заданным критериям. Расположение арендуемого и сдаваемого объекта считается значимым, если заданные критерии отличаются не более, чем на 10% (параметр «delta, %» в файле info.json). Также функция calc присваивает каждому объекту определенную цветовую гамму в зависимости от полученного результата, для дальнейшего отображения на карте.

def calc(dots, delta, status, indexes, crit0, crit1, deltacrit):
    onedegreex = 111*cos((68.942543+42.9846942)/2)
    onedegreey = 111
    col = 255
    step = 0.5*delta/255 # шаг, с которым меняется яркость точки
    leng = len(dots)
    neighbours = [[] for i in range(leng)]
    colors = [np.nan for i in range(leng)] # пустой массив цветов объектов
    while col >= 0:
        log('ЦВЕТ#' + str(255-col) + '/255 просчитан')
        deltax = delta/onedegreex
        deltay = delta/onedegreey
        for i in range(len(colors)):
            if np.isnan(colors[i]):
                indexlist = []
                j = i-1
	                while abs(dots[j][0] - dots[i][0]) <= deltax: # проверка разницы широт между объектами
                    if status[i]!=status[j]:
                        if abs(dots[j][1] - dots[i][1]) <= deltay: # проверка разницы долготы между объектами
                            indexlist.append(j)
                    j -= 1
                    if j < 0:
                        break
                j = i+1
                if j >= len(dots):
                    j = i
                while abs(dots[j][0] - dots[i][0]) <= deltax:
                    if status[i]!=status[j]:
                        if abs(dots[j][1] - dots[i][1]) <= deltay:
                            indexlist.append(j)
                    j += 1
                    if j >= len(dots):
                        break
# происходит анализ заданных критериев, с целью выявления нужных объектов 
                for j in indexlist:

                    d1 = abs((crit0[indexes[i]]+crit1[indexes[i]])-						  (crit0[indexes[j]]+crit1[indexes[j]]))

                    d2 = max((crit0[indexes[i]]+crit1[indexes[i]]),
					  (crit0[indexes[j]]+crit1[indexes[j]]))

                    if d1/d2*100<=deltacrit:

                        d = sqrt(((dots[i][0]-											   dots[j][0])*onedegreex)**2 + 
					      ((dots[i][1]-dots[j][1])
                                   *onedegreey)**2)
                        if d <= delta: # если разница критериев меньше заданной границы, объект считается значимым
                            colors[i] = col
                            neighbours[i].append(indexes[j])

        delta += step
        col -= 1
    return colors, neighbours

Арендуемые и сдаваемые объекты отличаются по назначению. Объект может использоваться как архив, офисное помещение, склад и т.д. (в файле info.json разные типы помещений обозначены как object_type1, object_type2, object_type3 и т.д.) Расчеты для каждого типа помещений производятся отдельно, автостоянка и склад не сравниваются между собой. Соответственно, максимальное расстояние между объектами для каждого типа задается отдельно в файле info.json:

for nameON, dist in deltadict.items():
# nameON – назначение объекта (склад, гараж, архив и т.д.)
# dist –  максимальное расстояние между объектами в километрах
        print(nameON, dist)
        log('<' + nameON + '> Начат процесс обработки')
        cur_objects, dots, status, indexes = dots_init(dictionary, 
database, nameON, dictionary_column, crit1, Len, Wid)
        dots, indexes, status = sortX(dots, indexes, status)
        dots, indexes, status = sortY(dots, indexes, status)
        colors, neighbours = calc(dots, dist, status, indexes, 
database[crit0], database[crit1], deltacrit)

На основании полученных результатов мы не выявили по арендуемым и сдаваемым объектам недвижимости значительных отклонений от заданных критериев. Все арендуемые и сдаваемые объекты находились друг от друга на расстояниях, соответствующих требованиям нашей организации. Но наша компания столкнулись с проблемой длительного поиска потенциальных арендаторов. Для ее решения при помощи геолокации мы сопоставили адреса своих свободных/высвобождаемых помещений с адресами дочерних/зависимых компаний, которые находятся рядом с нашими объектами и подходят по категории (офис, склад и т.д.). Для выполнения данной задачи использовали ранее разработанный инструмент, улучшенный новым алгоритмом – поиск возможных заинтересованных лиц в сети интернет с использованием «парсинга» интернет-сайтов организаций, для поиска адресов и координат объектов недвижимости:

req = requests.get('https:// адрес… ’) # получаем, код web-страницы
soup = BeautifulSoup(req.text, 'html.parser') 
# извлекаем url раздела, который индивидуален для каждого города 
tag_p = soup.find_all('p', attrs={'class':'color-green'}) 
urls = []
for p in tag_p:
    for tag_a in p.find_all('a'):
        urls.append(tag_a['href'])
locations = []
# извлекаем адреса представительств в каждом городе
for url in urls: 
    req = requests.get('https:// адрес… ’+url)
    soup = BeautifulSoup(req.text, 'html.parser')
    # парсим название города и адрес  
    city = soup.h2.string
    addr = str(soup.find('div', {'class':'wrap item-grid item-								grid-3'}).find_all('p')[1])
    addr = re.search(u'<br/>(.+?)</p>', addr).group(1)
    locations.append('{0}, {1}'.format(city, addr))

В случае, когда удавалось получить только адреса интересующих организаций, использовался код для определения координат по адресу:

addr['Url'] = ['https://www.google.com/maps/search/' + i for i in     			addr['Адрес']] # url для запроса координат на GoogleMap
Url_With_Coordinates = []
# настройка автоматического тестового ПО 
option = webdriver.ChromeOptions()
prefs = {'profile.default_content_setting_values': {'images':2, 				'javascript':2}}
option.add_experimental_option('prefs', prefs)
driver=webdriver.Chrome("D:\\ML\\sber\\chromedriver_win32\\chrome 						driver.exe", options=option)
# проход циклом по каждому адресу, отправляем запросу на www.google.com/maps с помощью тестового ПО
for url in tqdmn(addr.Url, leave=False):
driver.get(url)#получаем код страницы с координатами    Url_With_Coordinates.append(driver.find_element_by_css_selector(   				'meta[itemprop=image]').get_attribute('content'))
    driver.close()
  # парсим широту  
lat = re.findall(r'center=(\d+.\d+)%2C',''.join(Url_With_Coordinates))
# парсим долготу
long = re.findall(r'%2C(\d+.\d+)&zoom=',''.join(Url_With_Coordinates))

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

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

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

Полная версия кода доступна в репозитории на githab по ссылкам:

https://github.com/nikabliznetsova/GeoObjClustering

https://github.com/ilnuuur/sb_geo