Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
На днях мне позвонил друг и сказал, что хочет остановиться в Питере на пару-тройку дней и посмотреть старинные памятники архитектуры нашей культурной столицы. Спросил совета, — где бы ему остановиться поближе к центру города, чтобы успеть посмотреть Летний сад и все такое, это значит точно не у меня… фух
А поскольку буквально на днях я завершил вводную часть курса Аналитик данных в Яндекс Практикум на одной из онлайн площадок, то и решил потренироваться на друге в применении логики такого анализа. Забегая вперед, скажу, что результат меня несколько удивил, возможно где-то в моей логике ошибка. Если так, то поправьте меня. Я только учусь.
Итак поехали…
Загрузка датасета
На сайте https://data.gov.ru/ находим и скачиваем датасет Объекты культурного наследия на территории Санкт-Петербурга. Последнее обновление датировано 2016 годом, но для объектов возрастом в пару веков это не будет проблемой.
Давайте оценим количество данных, с которыми будем работать:
import pandas
data = pandas.read_csv('spb_memo.csv')
print('Количество строк:', len(data))
Количество строк: 9275
Ого, более девяти тысяч объектов культурного наследия! Недаром наш город называют культурной столицей России. Что есть внутри?
data.head()
number | name | name_object | date | author | address | district | protection_category | base | note | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | NaN | Здание Консисторского управления Могилевской Р... | 1870-1873; 1878-1879; 1896-1897; 1900-1902 | арх. В.И. Собольщиков, Е.С. Воротилов; арх. Е.... | 1-я Красноармейская ул., 11, лит. А, Б | Адмиралтейский | Объект культурного наследия регионального знач... | Распоряжение КГИОП № 10-22 от 21.07.2009 | NaN |
Выборка по районам
У нас есть названия объектов, даты постройки, адреса и даже имена авторов, отлично!
Как правило, городские достопримечательности, по большей части, располагаются ближе к центру города. Но любая гипотеза требует доказательств. Посмотрим статистку по районам:
districts = list(data['district'])
districts_unique = list(set(districts))
total_per_district = []
for district in set(districts):
district_counter = 0
for index in range(len(districts)):
if districts[index] == district:
district_counter += 1
total_per_district.append(district_counter)
seaborn.barplot(x=total_per_district, y=districts_unique)
Никаких сюрпризов. Большинство памятников архитектуры располагаются в Центральном, Петродворцовом и Адмиралтейских районах города. Это самый что ни на есть центр. На периферии объектов значительно меньше, за исключением Пушкинского района. Но вряд ли за свои короткие выходные друг поедет так далеко, хотя и много потеряет. Как бы там ни было, вычёркиваем Пушкинский район, а также другие районы, находящиеся за КАДом.
outside_districts = ['Пушкинский', 'Кронштадтский', 'Кронштадт',
'Колпинский', 'Курортный', 'Приморский', 'Санкт-Петербург']
districts_unique = [item for item in districts_unique
if item not in outside_districts ]
for district in districts_unique:
district_counter = 0
for index in range(len(districts)):
if districts[index] == district:
district_counter += 1
total_per_district.append(district_counter)
seaborn.barplot(x=total_per_district, y=districts_unique)
Проверка других столбцов
Что ж, круг сужается, двигаемся дальше. В датасете есть такая характеристика объектов, как протекционная категория. Будет ли нам полезна эта колонка?
protection_categories = list(data['protection_category'])
protection_categories_unique = list(set(protection_categories))
total_per_category = []
for category in set(protection_categories):
category_counter = 0
for index in range(len(districts)):
if protection_categories[index] == category:
category_counter += 1
total_per_category.append(category_counter)
seaborn.barplot(x=total_per_category, y=protection_categories_unique)
Возможно информация важная, на для нашей задачи тут ничего полезного. Всего три категории, по которым мы не собираемся сортировать данные. Попробуем другой путь.
Выборка по авторам
Итак, что мы пока имеем. Поиск у нас сузился до центральных районов. Там больше всего интересующих нас объектов. Но "больше" означает ли "лучше"? Посмотрим статистку по авторам всех архитектурных произведений города. Есть небольшая сложность в том, что у каждого объекта авторы перечислены просто через запятую, иногда с их должностями, иногда просто ФИО. Пришлось исправить это небольшим фильтром, чтобы остались только ФИО.
authors_all = list(data['author'])
authors = []
total_per_author = []
for author_line in authors_all:
if author_line == author_line:
if ',' in author_line:
for author in author_line.replace(';', ',').replace('арх. ', '').replace('худ. ', '').replace('гражд.инж. ', '').replace('архитекторы ', '').replace('фонтанный мастер ', '').replace('арх-ры ', '').split(','):
author = author.strip()
if author not in authors:
authors.append(author.strip())
total_per_author.append(1)
else:
index = authors.index(author.strip())
total_per_author[index] += 1
else:
if author not in authors:
authors.append(author.strip())
authors_df = pandas.DataFrame(authors, columns=['name'])
authors_df['count'] = total_per_author
seaborn.barplot(x=authors_df['count'],
y=authors_df['name'])
Этот график совсем не выглядит информативным. Чисто для целей визуализации предлагаю посмотреть тех авторов, которые построили более 20 объектов. Меняем последние строчки кода на:
most_frequent_authors_df = authors_df.loc[(authors_df['count'] > 20)]
seaborn.barplot(x=most_frequent_authors_df['count'],
y=most_frequent_authors_df['name'])
Чем дальше, тем интересней! И кто же этот Г.А. Симонов, который судя по статистике отстроил чуть ли не половину города, а в честь него даже ни одной улицы не назвали? Заглянем в Википедию:
Григорий Александрович Симонов (23 января 1893, Ташкент — 31 января 1974, Москва) — советский архитектор, инженер, педагог.
Г. А. Симонов родился в Ташкенте. Детство провел в Троицке, там окончил гимназию.
Окончил Петроградский Институт гражданских инженеров в 1920 году, где преподавал с 1929 года.
В 1919—1922 гг. обучался в Академии Художеств.
С 1924 года руководил Проектным Бюро Стройкома.
С 1943 года — заместитель председателя Государственного Комитета по делам архитектуры при Совете Народных Комиссаров СССР.
С 1947 по 1949 год — председатель Комитета по делам архитектуры при Совмине СССР.
С 1955 года — преподаватель Московского архитектурно-строительного института.
Теперь понятно, это послереволюционный, советский архитектор, руководивший сооружением множества объектов в тогдашнем Ленинграде. Если отставить личность товарища Симонова в покое, что еще мы здесь видим? Имеет место выброс статистических данных — Симонов упоминается так часто, что затмевает других. К тому же друг попросил старинные памятники архитектуры. Так что, ни в коем случае не умаляя творцов последнего века, давайте исключим из выборки все сооружения старше 1900 года. Правда тут есть небольшая проблема...
В нашем датасете столбец с датами — текстовый, произвольной длинны и содержания. Где-то стоит одна дата, где-то период, где-то просто двухзначная цифра века постройки. В данном случае мы не можем сделать числовую выборку, а можем работать только со строками. Если бы точность была важна, можно было бы преобразовать этот столбец, как мы сделали с авторами, можно было бы отбирать строки регулярными выражениями. Но это всего лишь поездка друга, так что решаем не усложнять, и волевым решением отсекаем все даты, где встречается число 19. Мы упустим несколько построек, где в поле дата указано "конец 19 в.", "1819" и тому подобных. Примем это как допустимые потери.
districts_authors_df = pandas.DataFrame(districts_unique, columns=['district'])
lat = []
lon = []
for col in most_frequent_authors_df['name']:
districts_authors_df[col] = 0
districts_authors_df.set_index('district', inplace=True)
top_poi = []
for district in districts_unique:
district_df = data.loc[(data['district'] == district) & (~data["date"].str.contains('19', na=True))]
for index, row in district_df.iterrows():
for author in most_frequent_authors_df['name']:
if row['author'] == row['author'] and author in row['author']:
districts_authors_df[author][district] += 1
top_poi.append(row['number'])
districts_authors_df.drop('автор не установлен', axis=1, inplace=True)
#districts_authors_df = districts_authors_df.loc[(districts_authors_df>5).any(axis=1)] #!=0
seaborn.heatmap(districts_authors_df, xticklabels=True, yticklabels=True) #, annot=True
Ситуация проясняется. У нас теперь есть список наиболее интересных для нашей задачи районов и авторов. Переменная top_poi
(Top Points of Interests) содержит номера наших призеров. Но где именно находятся объекты? Пришло время для геокодирования...
Геокодирование
У нас есть колонка с адресами объектов, но если мы хотим определить, как далеко или близко что-то находится, нам нужны координаты. Процесс конвертации адресов в координаты называется геокодирование или геокодинг. У Яндекса есть отличный сервис, который все сделает за нас.
Для начала сделаем копию нашего датасета и добавим туда два новых поля: долгота и широта:
df = pandas.read_csv('spb_memo.csv')
df['lat'] = float('nan')
df['lon'] = float('nan')
df.to_csv("spb_memo_geo.csv", index=False)
У геокодера Яндекса бесплатно только 1000 запросов в сутки. В нашей переменной top_poi
содержится чуть меньше 500 объектов, так что на это исследование у нас сегодня хватает:
df = pandas.read_csv('spb_memo_geo.csv')
from decimal import Decimal
import os
from dotenv import load_dotenv
from yandex_geocoder import Client
load_dotenv('.env')
yandex_geo_api_key = os.environ.get("yaGeoApi")
client = Client(yandex_geo_api_key)
coordinates = 0
api_limit_per_day = 1000
for poi in top_poi:
if api_limit_per_day > 0:
poi_row = df.loc[(df['number'] == poi)]
if poi_row.empty:
lat = float('nan')
else:
lat = list(poi_row['lat'])[0]
addr = list(poi_row['address'])
if lat != lat and len(addr) > 0 and addr[0] == addr[0] and len(addr[0]) > 5:
coords = client.coordinates("Санкт-Петербург, " + addr[0])
df.loc[poi_row.index, 'lon'], df.loc[poi_row.index, 'lat'] = coords
api_limit_per_day -= 1
df.to_csv("spb_memo_geo.csv", index=False)
Последний отсев
Итак, наш усовершенствованный датасет теперь содержит долготу и широту для интересующих нас объектов. Визуализируем его в виде красных точек:
df = pandas.read_csv('spb_memo_geo.csv')
lat_lon = df[df['number'].isin(top_poi)]
x = list(lat_lon['lat'])
y = list(lat_lon['lon'])
seaborn.scatterplot(x=x, y=y, c=['red'])
Вы будете правы, если скажите, что такое расположение точек не соответствует сторонам света — по X должна быть долгота, а по Y — широта. Но на данном этапе нам это и не нужно. Мы ищем зависимости, отклонения, совпадения и т.д.
И что это за одинокая точка в левом верхнем углу, из-за которой у нас верхняя половина графика пустая?
print(df.loc[(df['lat'] < 59.8) & (df['lon'] > 31)])
7922 Летний сад
Name: name, dtype: object
Ах, это Летний сад, который друг упоминал в своей просьбе. Туда он пойдет независимо от расстояния. Вычеркиваем его из выборки, к тому же для нас это небольшой выброс данных, делающий картину менее понятной.
lat_lon = df.loc[df['number'].isin(top_poi) & (df['lat'] > 59.6) &
(df['lat'] < 62) & (df['lon'] > 29) & (df['lon'] < 30.8)]
x = list(lat_lon['lat'])
y = list(lat_lon['lon'])
seaborn.scatterplot(x=x, y=y, c=['red'])
Итог
Итак, пришло время ответить на самый главный вопрос нашего исследования. Раз друг хочет поселиться где-то в центре, чтобы независимо от того какой объект он выберет, это было относительно недалеко, просто возьмем среднее значение всех координат.
xy_center = (sum(x) / len(x), sum(y) / len(x))
seaborn.scatterplot(x=y, y=x, c=['red'])
seaborn.scatterplot(x=[xy_center[1]], y=[xy_center[0]], c=['green'])
Зеленая точка и есть наше заветное место. Интересно, а где это вообще?
print(xy_center[1], ',', xy_center[0])
59.926768, 30.294057
Снова прибегнем к геокодеру Яндекса, но на этот раз в обратном направлении — для преобразования координат обратно в адрес:
address = client.address(Decimal("30.294057"), Decimal("59.926768"))
print(address)
Россия, Санкт-Петербург, набережная Крюкова канала
Вот оно! Когда все только началось, лично мне казалось, что центральная точка окажется где-то на Дворцовой площади или в Петропавловской крепости, но нет, другие архитектурные объекты оттянули ее на себя, и мы получили набережную Крюкова канала.
Что до друга, он взял этот адрес за точку отсчета, нашел где можно остановиться поблизости и провел в нашем городе незабываемые выходные.
Финальная визуализация
Это было чисто статистическое исследование, — максимум, что удалось выжать из этого датасета. Исследование не имело отношение к истории. Мы не узнали, какие архитектурные стили преобладают в Санкт-Петербурге, и как они менялись со временем; не увидели в выборках архитекторов-основателей. Надеюсь обо всем этом друг узнал из посещенных им экскурсий.
Напоследок, давайте сделаем все красиво, и действительно сопоставим наши данные с реальной картой, используя библиотеку OSMnx, основанную на данных OpenStreetMap (спасибо Carlos Lannister за подсказку):
import osmnx as ox
# Center of map
latitude = 59.939099
longitude = 30.315877
point = (latitude, longitude)
G = ox.graph_from_point(point, dist=10000, retain_all=True, simplify = True,
network_type='all')
u = []
v = []
key = []
data = []
for uu, vv, kkey, ddata in G.edges(keys=True, data=True):
u.append(uu)
v.append(vv)
key.append(kkey)
data.append(ddata)
# List to store colors
roadColors = []
roadWidths = []
for item in data:
if "length" in item.keys():
if item["length"] <= 100:
linewidth = 0.10
color = "#a6a6a6"
elif item["length"] > 100 and item["length"] <= 200:
linewidth = 0.15
color = "#676767"
elif item["length"] > 200 and item["length"] <= 400:
linewidth = 0.25
color = "#454545"
elif item["length"] > 400 and item["length"] <= 800:
color = "#d5d5d5"
linewidth = 0.35
else:
color = "#ededed"
linewidth = 0.45
else:
color = "#a6a6a6"
linewidth = 0.10
roadColors.append(color)
roadWidths.append(linewidth)
bgcolor = "#061529"
fig, ax = ox.plot_graph(G, node_size=0,figsize=(27, 40),
dpi = 300,bgcolor = bgcolor,
save = False, edge_color=roadColors,
edge_linewidth=roadWidths, edge_alpha=1,
show=False, close=False)
for i in range(len(x)): #
ax.scatter(y[i], x[i], s = 100, c='red')
ax.scatter(xy_center[1], xy_center[0], s = 100, c='green')
Как видно из кода, красные точки — это объекты культурного наследия Санк-Петербурга (в данном случае порядка 1 300 объектов, без ограничения по годам), а зеленая точка — рекомендованное место старта.
Если захотите проверить мои расчёты, можете найти все файлы в моём репозитории Github.