#!pip3 install geopandas
#!pip3 install geopy
#!pip3 install folium
#!pip3 install branca
1.1 geopy - основная билбиотека, которая будет использоваться в проекте для прямого геокодирования с помощью API Yandex¶
import numpy as np
import pandas as pd
import time
from pprint import pprint
from geopy.geocoders import ArcGIS,Yandex
from geopy.extra.rate_limiter import RateLimiter
# instantiate a new Nominatim client
app = ArcGIS(user_agent="tutorial")
import warnings
warnings.filterwarnings('ignore')
1.2 Так как прямое геокодирование большого количества адресов достаточно долгая процедура, в качестве теста, добавим оповещение в Telegramm (это позволит в цикле каждые, например, 10 000 адресов, отправлять сообщение с статусом работы)¶
import requests
# telegram url
bot_token = "TelegramBotApiToken"
chat_id = "TelegramChatID"
def send_mess(text):
params = {'chat_id':chat_id, 'text': text}
response = requests.post('https://api.telegram.org/bot'+ bot_token + '/sendMessage', data=params)
return response
send_mess("Hello world!")
<Response [200]>
1.3 Загрузка первичной базы с адресами 500 строк (Публичный источник: Адресный справочник Челябинска - https://t.domspravka.com/, дополнительно поля name и birth изменены)¶
data = pd.read_csv("D:\\data_test.csv", sep=";",encoding="windows-1251",index_col=False)#cp866
data.head(3)
name | birth | adress | kv | example_feature | |
---|---|---|---|---|---|
0 | ЮЛИЯ | 10.10.1983 | 454080 ПР-КТ. ЛЕНИНА, д. 83 | - | example_email1983@gmail.com |
1 | СЕРГЕЙ | 06.09.1986 | 454080 ПР-КТ. ЛЕНИНА, д. 83 | - | example_email1986@gmail.com |
2 | ЕЛЕНА | 06.03.1977 | 454080 ПР-КТ. ЛЕНИНА, д. 83/А | 1 | example_email1977@gmail.com |
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 499 entries, 0 to 498 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 499 non-null object 1 birth 499 non-null object 2 adress 499 non-null object 3 kv 499 non-null object 4 example_feature 251 non-null object dtypes: object(5) memory usage: 19.6+ KB
#Очистка от аномалий и форматирование полей в датафрейме
data['name'] = data['name'].astype(str)
data['name'] = data['name'].str.replace('/n','')
data['adress'] = data['adress'].str.replace('/n','')
#data = data.infer_objects()
#Для более точного прямого геокодирования добавим значения страны, области и города в строку адреса
data['adress']= "РОССИЯ, Челябинская область, г. Челябинск"+ ", " +data['adress']
#Пример дополненного адреса
data['adress'][1]
'РОССИЯ, Челябинская область, г. Челябинск, 454080 ПР-КТ. ЛЕНИНА, д. 83'
1.4 Подключение Геокодер API Яндекс.Карт и проверка работы прямого геокодирования Яндекс API (https://yandex.ru/dev/maps/geocoder/)¶
#https://pikabu.ru/story/yandeks_baunti_ili_klyuch_za_million_besplatno_7737687 может помочь
app = Yandex(user_agent="tutorial",api_key="ApiKey")
location = app.geocode("454080 ПР-КТ. ЛЕНИНА, д. 83") #Тест работы прямого геокодинга
pprint(location)
# min_delay_seconds - Задержка на вызов (таким образом исключаем ошибки при слишком большой скорости запросов к геокодеру)
geocode = RateLimiter(app.geocode, min_delay_seconds=0.3)
Location(проспект Ленина, 83, Челябинск, Россия, (55.158637, 61.371382, 0.0))
#Создаем новый столбец для записи совокупных данных геокодера
data['location'] = 0
data['location'] = data['location'].astype(str)
#Создаем новые столбцы для конкретных числовых значений определенных координат
data['latitude'] = 0
data['longitude'] = 0
data['altitude'] = 0
data['point'] = 0
1.5 Применение геокодера Яндекс для получения координат адресов из загруженного списка¶
# Прямой геокодинг
for i in range(1,len(data)):
if (data['location'][i-1]==data['location'][i] and data['location'][i-1]!='0'):#Проверяем не совпадает ли следующая строка с предыдущей, если одинаковые, то просто приравниваем их друг к другу
data['location'][i-1]=data['location'][i]
else:
data['location'][i:(i+1)] = data['adress'][i:(i+1)].apply(geocode) #Цикличное геокодирование
data['point'][i:(i+1)] = data['location'][i:(i+1)].apply(lambda loc: tuple(loc.point) if loc else (0.0,0.0,0.0)) #None)
#if (i % 10==0):
# print (data[['location']][(i-10):i])
if (i % 100==0):#Актуально для 10 000 и более, 100 взято в качестве теста
send_mess("("+ str(i) + ") строк обработано") #Сообщение с статусом в Telegram чат
1.6 Заполнение полей конкретными числовыми значениями координат (разделение столбца 'point' на столбцы 'latitude', 'longitude','altitude')¶
data['latitude']=data['latitude'].astype(float)
data['longitude']=data['longitude'].astype(float)
data['altitude']=data['altitude'].astype(float)
a = data['point'].tolist()# Формирование из столбца датафрейма 'point' списка с значениями
b = []
z = 0
for n in range(1,len(data)):
b.append(str(a[n]).split(','))# Дробим список с значениями 'point' по запятым и записываем части в отдельные столбцы
data['latitude'][n]=float(b[n-1][0][1:50])
data['longitude'][n]=float(b[n-1][1][1:50])
data['altitude'][n]=float(b[n-1][2][1:3])
#print(n,data['latitude'][n],data['longitude'][n])
data.head()# Итоговый датафрейм с данными
name | birth | adress | kv | example_feature | location | latitude | longitude | altitude | point | |
---|---|---|---|---|---|---|---|---|---|---|
0 | ЮЛИЯ | 10.10.1983 | РОССИЯ, Челябинская область, г. Челябинск, 454... | - | example_email1983@gmail.com | 0 | 0.000000 | 0.000000 | 0.0 | 0 |
1 | СЕРГЕЙ | 06.09.1986 | РОССИЯ, Челябинская область, г. Челябинск, 454... | - | example_email1986@gmail.com | (проспект Ленина, 83, Челябинск, Россия, (55.1... | 55.158637 | 61.371382 | 0.0 | (55.158637, 61.371382, 0.0) |
2 | ЕЛЕНА | 06.03.1977 | РОССИЯ, Челябинская область, г. Челябинск, 454... | 1 | example_email1977@gmail.com | (проспект Ленина, 83А, Челябинск, Россия, (55.... | 55.158616 | 61.372235 | 0.0 | (55.158616, 61.372235, 0.0) |
3 | САША | 08.04.1994 | РОССИЯ, Челябинская область, г. Челябинск, 454... | 2 | example_email1994@gmail.com | (проспект Ленина, 83А, Челябинск, Россия, (55.... | 55.158616 | 61.372235 | 0.0 | (55.158616, 61.372235, 0.0) |
4 | ГАЛИНА | 15.01.1947 | РОССИЯ, Челябинская область, г. Челябинск, 454... | 2 | NaN | (проспект Ленина, 83А, Челябинск, Россия, (55.... | 55.158616 | 61.372235 | 0.0 | (55.158616, 61.372235, 0.0) |
2.1 folium - основная билбиотека, которая будет использоваться в проекте для создания интерактивных карт¶
import folium
from folium.plugins import FastMarkerCluster
from folium.plugins import MarkerCluster
from folium.plugins import Search
from folium import FeatureGroup
import branca
from datetime import timedelta, datetime
# 1 - Добавляем столбец с именем и номером квартиры (для дальнейшего облегчения поиска по фильтру на карте)
# 2 - Сортируем датафрейм по № квартиры и адресу (для дальнейшего удобного отображения марекров в составе кластера маркеров)
data.sort_values(by=['kv','adress'])
data['name_kv'] = data['name']+" //"+data['kv']
data['emailkv'] = data['example_feature']+" //"+data['kv']
2.2 Создание функции для формирования HTML таблиц с значениями из датафрейма (при клике по значку на карте откроется данная таблица с детальной информацией. Формат приведен для примера, реально применение всей палитры возможностей языка HTML)¶
def fancy_html(row):
i = row
FIO = data['name'].iloc[i]
age = data['birth'].iloc[i]
adress = data['adress'].iloc[i]
adress_geo = data['location'].iloc[i]
kv = data['kv'].iloc[i]
email = data['example_feature'].iloc[i]
left_col_colour = "#2A799C"
right_col_colour = "#C5DCE7"
# Простая HTML таблица с данными из датафрейма
html = """<!DOCTYPE html>
<html>
</head>
<table style="height: 130px; width: 310px;">
<tbody>
<tr>
<td style="background-color: """+ left_col_colour +""";"><span style="color: #ffffff;">ФИО жильца</span></td>
<td style="width: 200px;background-color: """+ right_col_colour +""";">{}</td>""".format(FIO) + """
</tr>
<tr>
<td style="background-color: """+ left_col_colour +""";"><span style="color: #ffffff;">Год рождения</span></td>
<td style="width: 200px;background-color: """+ right_col_colour +""";">{}</td>""".format(age) + """
</tr>
<tr>
<td style="background-color: """+ left_col_colour +""";"><span style="color: #ffffff;">Номер помещения</span></td>
<td style="width: 200px;background-color: """+ right_col_colour +""";">{}</td>""".format(kv) + """
</tr>
<tr>
<td style="background-color: """+ left_col_colour +""";"><span style="color: #ffffff;">E-mail</span></td>
<td style="width: 200px;background-color: """+ right_col_colour +""";">{}</td>""".format(email) + """
</tr>
<tr>
<td style="background-color: """+ left_col_colour +""";"><span style="color: #ffffff;">Адрес(полный)</span></td>
<td style="width: 200px;background-color: """+ right_col_colour +""";">{}</td>""".format(adress) + """
</tr>
<tr>
<td style="background-color: """+ left_col_colour +""";"><span style="color: #ffffff;">Адрес по геодекодингу</span></td>
<td style="width: 200px;background-color: """+ right_col_colour +""";">{}</td>""".format(adress_geo) + """
</tr>
</tbody>
</table>
</html>
"""
return html
2.3 Создание функций для динмаической окраски значков на карте по заданным условиям (дата рождения и есть ли e-mail)¶
def color_change(age):
if(datetime(2000, 1, 1) < age):
return('darkgreen')
elif(datetime(1980, 1, 1) <= age < datetime(2000, 1, 1)):
return('green')
elif(datetime(1960, 1, 1) <= age < datetime(1980, 1, 1)):
return('orange')
elif(datetime(1930, 1, 1) <= age < datetime(1960, 1, 1)):
return('red')
elif(age > datetime(1930, 1, 1)):
return('darkred')
else:
return('gray')
def back_color_change(email_flag):
if(email_flag == False):
return('#FFD700')
else:
return('#FFFFE0')
#7FFFD4 - циан
#FFD700 - золото
lat = data['latitude']
lon = data['longitude']
location1 = data['adress']
location2 = data['location']
age = pd.to_datetime(data['birth']) # Преобразование столбца с датой рождения в формат datetime (для сравнения с другими датами)
#data['birth'] = data['birth'].dt.strftime('%Y-%m-%d') # Преобразование столбца с датой рождения в формат str (для отрисовки на карте)
email=data['example_feature'].copy()
email=email.isnull()
if (z==0): #Конструкция нужна, чтобы функционировала предыдущая проверка isnull, т.к. после замены nan на "" значение перестанет быть nan
data['example_feature'] = data['example_feature'].fillna('') # Убираем nan из отображения на карте
z=1
else:
pass
2.4 Преобразование данных датафрейма в geojson для последующего использования в качестве входных данных построения карты¶
import requests, json
def df_to_geojson(df, properties, lat='latitude', lon='longitude'):
# create a new python dict to contain our geojson data, using geojson format
geojson = {'type':'FeatureCollection', 'features':[]}
# loop through each row in the dataframe and convert each row to geojson format
for _, row in df.iterrows():
# create a feature template to fill in
feature = {'type':'Feature',
'properties':{},
'geometry':{'type':'Point',
'coordinates':[]}}
# fill in the coordinates
feature['geometry']['coordinates'] = [row[lon],row[lat]]
# for each column, get the value and add it as a new feature property
for prop in properties:
feature['properties'][prop] = row[prop]
# add this feature (aka, converted dataframe row) to the list of features inside our dict
geojson['features'].append(feature)
return geojson
geolist=data.columns.tolist()
geolist.remove('location')
geolist.remove('latitude')
geolist.remove('longitude')
cols=geolist
geojson = df_to_geojson(data, cols)
2.5 Создание самого объекта map библиотеки folium, инициализация начальных парметров (приближение/стиль карт) и фильтров для поиска по ключевым значениям¶
map1 = folium.Map(
location=[55.159563, 61.375695], # Начальная позиция карты
tiles='OpenStreetMap',#"Mapbox bright" - Стиль карты
zoom_start=16, # Начальное приближение карты
control_scale=False,
prefer_canvas=True
)
marker_cluster = MarkerCluster(name="Markers_GEO",overlay=True,show=False).add_to(map1)
marker_cluster2 = MarkerCluster(name="Markers",overlay=True).add_to(map1)
geo = folium.GeoJson(
geojson,
name="adress",
show=True,
tooltip=folium.GeoJsonTooltip(
fields=["adress", "name_kv","emailkv"],
aliases=["adress", "name_kv","emailkv"], localize=True
),
).add_to(marker_cluster)
#Добавление поискового фильтра по адресу
adresssearch = Search(
layer=marker_cluster,
geom_type="Point",
placeholder="Поиск по адресу",
collapsed=True,
search_label="adress",
search_zoom=15
).add_to(map1)
#Добавление поискового фильтра по ФИО
FIOsearch = Search(
layer=marker_cluster,
geom_type="Point",
placeholder="Поиск по ФИО",
collapsed=True,
search_label="name_kv",
search_zoom=15
).add_to(map1)
#Добавление поискового фильтра по e-mail
Email_search = Search(
layer=marker_cluster,
geom_type="Point",
placeholder="Поиск по E-mail",
collapsed=True,
search_label="emailkv",
search_zoom=15
).add_to(map1)
folium.LayerControl().add_to(map1)
<folium.map.LayerControl at 0x2248f6ca7c0>
2.6 Отрисовываем на карте все элементы из geojson, в соответствии с заданными параметрами (используя функции color_change и back_color_change для цветовой индикации)¶
for p in range(0,len(data)):
html = fancy_html(p) #Получаем данные из HTML для попапов
iframe = branca.element.IFrame(html=html,width=330,height=270) #Размер объекта попапа
popup = folium.Popup(iframe,parse_html=True)
folium.map.Marker(location=[lat.iloc[p], lon.iloc[p]],
icon=folium.plugins.BeautifyIcon(border_color = color_change(age.iloc[p]),# Цвет границы (в соответствии с датой рождения)
border_width = 1, #Ширина границы
background_color = back_color_change(email.iloc[p]), #Цвет подложки значка (если есть email, то золотой)
text_color = 'dark', #Цвет текста внутри значка
number = data["kv"].iloc[p], # Текст внутри значка
icon_shape = 'marker'), # Формат значка
popup=popup, # Всплывающий элемент при нажатии на значок
tooltip = '{0}<br>{1}'.format(data["name"].iloc[p], data["example_feature"].iloc[p])# Всплывающий элемент при наведении мышкой на значок
).add_to(marker_cluster2)
if (p % 100==0):
print(p)
0 100 200 300 400
2.7 Финальная визуализация карты¶
map1
map1.save("Yours_path_here.html")