На днях мне позвонил друг и сказал, что хочет остановиться в Питере на пару-тройку дней и посмотреть старинные памятники архитектуры нашей культурной столицы. Спросил совета, — где бы ему остановиться поближе к центру города, чтобы успеть посмотреть Летний сад и все такое, это значит точно не у меня… фух
А поскольку буквально на днях я завершил вводную часть курса Аналитик данных в Яндекс Практикум на одной из онлайн площадок, то и решил потренироваться на друге в применении логики такого анализа. Забегая вперед, скажу, что результат меня несколько удивил, возможно где-то в моей логике ошибка. Если так, то поправьте меня. Я только учусь.
Итак поехали…
Загрузка датасета
На сайте 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'])
Этот график совсем не выглядит информативным. Сколько же там авторов:
print('Авторов в выбоке:', len(authors_df))
Авторов в выбоке: 2011
Конечно, для такого количества авторов столбчатую диаграмму не построишь. Чисто для целей визуализации предлагаю посмотреть тех авторов, которые построили более 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'])
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['protection_category'] == 'Объект культурного наследия федерального значения') &
(~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
for author in authors_df['name']:
if row['author'] == row['author'] and author in row['author']:
top_poi.append(row['number'])
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 объектов, без ограничения по годам), а зеленая точка — рекомендованное место старта.
Где же мистика?
Собрав все правки, пожелания и рекомендации из комментариев я еще раз переделал расчёты:
Убрал из выборки Петродворцовый район
Добавил Приморский район
Ограничил выборку только объектами имеющими федеральное значение
Убедился, что значимые архитекторы города, такие как Бенуа, Растрелли, Трезини и др. включены в выборку
В результате всех этих коррекций точка сместилась на адрес Россия, Санкт-Петербург, Инженерная улица, 2-4А, тот самый мистический Инженерный замок, с его призраком, легендами и загадочной надписью на фасаде. Ну чем не мистика?
Ниже обновленная финальная визуализация со всеми коррекциями и новой центральной точкой:
Если захотите проверить мои расчёты, можете найти все файлы в моём репозитории Github.
Комментарии (18)
aborouhin
01.07.2022 00:06+6Хм, на северо-востоке плотный отдельностоящий кластер памятников - это, похоже, больница Петра Великого, которая в реестре, видимо, разбита чуть ли не на отдельные сараи. Ваш друг точно захочет её осматривать?.. :) На юго-востоке Пролетарский завод - аналогичная ситуация и вопрос. А вот что за аналогичное скопление на юго-западе - не пойму, это, вроде, квартал между ул. Зенитчиков, Зайцева, Краснопутиловской и пр. Стачек - обычные сталинки, точно такие же, как в соседних кварталах.
А вот эти три отделённых, но богатых "памятниками" пятна, похоже, сильно поспособствовали смещению точки размещения в место, от которого до ближайшего памятника ещё прогуляться надо :)
alcanoid
01.07.2022 00:53Да ладно, прямо на Крюковом стоят Новая Голландия, Мариинка, Никольский собор, Никольский рынок и десяток-другой памятников помельче.
Но в целом замечание толковое, поддерживаю.
aborouhin
01.07.2022 00:58Не, ну я-то знаю, что там находится, но вот на карте автора поста, к слову говоря, красных точек на этих объектах нет... то ли дело Пролетарский завод :)
Shaman_RSHU
01.07.2022 13:06+1Это квартал №1 в Автово. Первые сталинки, как группа домов в едином микрорайоне. Но например в Московском районе есть аналогичные кварталы, которые были построены даже раньше и про них можно рассказать побольше, но на карте они почему-то не обозначены.
aborouhin
01.07.2022 13:13+1Учитывая, что автор поста отсекал строения младше 1900 года (весьма варварским способом, NER - нет, не слышал :), подозреваю, что конкретно у этого квартала в реестре даты постройки как-то своеобразно записаны оказались.
alcanoid
01.07.2022 00:31+9Этот график совсем не выглядит информативным. Чисто для целей
визуализации предлагаю посмотреть тех авторов, которые построили более
20 объектов.Поздравляю! В вашем списке всего четыре-пять значимых фамилий, зато крайне удачно исключены (так, чисто навскидку) Бенуа, Валлен-Деламот, Воронихин, Кваренги, Монферран, Растрелли, Тома де Томон, Трезини. Не дотянули.
StjarnornasFred
01.07.2022 07:36+1Отличное исследование для познавательных целей. Но путешествовать по такому путеводителю может только айтишник :)
Хорошо заметен предсказуемый эффект несущкствующего среднего: непосредственно возле точки "центра всех достопримечательностей" их самих почти нет.
А главная ошибка пошла вот отсюда:
В датасете есть такая характеристика объектов, как протекционная категория. Будет ли нам полезна эта колонка? <...> Возможно информация важная, на для нашей задачи тут ничего полезного. Всего три категории, по которым мы не собираемся сортировать данные.
А меж тем это и есть та самая важная и полезная величина, которая позволяет очистить список от больничных корпусов и сталинок, которые хоть и важны с историко-краеведчиской точки зрения, но малоинтересны для туризма.
VZ1 Автор
02.07.2022 15:56Это действительно оказалось одной и самых важных характеристик. Сделал выборку по объектам федерального значения, точка сместилась на Инженерный замок. В итоге исправил большую часть статьи и даже заголовок
CrazyElf
01.07.2022 09:54+1Вообще, например, весь первый фрагмент кода можно заменить на:
seaborn.countplot(y=data['district'])
, никакие "ручные" вычисления не нужны. А если бы и нужны были, то есть такая штукаdata['district'].value_counts()
, она опять же сама посчитает сколько раз какое значение встречается в данных. Если вы при использованииPandas
что-то считаете с помощью циклов, то нужно сразу задуматься, что тут не так. Обычно это вообще не нужно, все необходимые аггрегации туда уже встроены в готовом виде.
aik
01.07.2022 12:38+2Архитекторов по количеству выкидывать на стоило. Вроде могли пропасть такие, которые построили всего одно строение, но при этом очень ценное.
Ещё по 1900 зря отсечка сделана, лучше было бы по 1918, к примеру. Или по 1914 хотя бы, чтобы начало 20 века зацепить.
alcanoid
Просили поправок? Получите.
На этом я перестал читать. Вы бы карту хоть посмотрели для начала. Петродворцовый район, конечно, расположен внутри КАДа, но к центру города отношение он имеет крайне посредственное.
Shaman_RSHU
Из этого можно сделать вывод, что мало владеть инструментом. Можно близко к идеалу все посчитать и доверить алгоритмам анализ. Но итог нужно подводить, разбираясь в предметной области, по которой проводится анализ.
VZ1 Автор
Да, Ваш вывод корректен. Несколько раз уже слышал, что аналитик более ценен, когда он хорошо знаком с анализируемой им областью. Взял для анализа исторические ценности, придется залезть и подучить историю
VZ1 Автор
Да, спасибо за правки, тут Вы абсолютно правы. Исключил Петродворцовый район из выборки, и точка ожидаемо сместилась ближе к центру города. Исправил этот момент в статье