Pull to refresh

Мистика культурных ценностей Петербурга глазами аналитика

Reading time10 min
Views4.6K

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

А поскольку буквально на днях я завершил вводную часть курса Аналитик данных в Яндекс Практикум на одной из онлайн площадок, то и решил потренироваться на друге в применении логики такого анализа. Забегая вперед, скажу, что результат меня несколько удивил, возможно где-то в моей логике ошибка. Если так, то поправьте меня. Я только учусь.

Итак поехали…

Загрузка датасета

На сайте 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'])
Архитекторы Санкт-Петербурга построившие 20 или более культурных объектов
Архитекторы Санкт-Петербурга построившие 20 или более культурных объектов

Чем дальше, тем интересней! И кто же этот Г.А. Симонов, который судя по статистике отстроил чуть ли не половину города, а в честь него даже ни одной улицы не назвали? Заглянем в Википедию:

Григорий Александрович Симонов (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 объектов, без ограничения по годам), а зеленая точка — рекомендованное место старта.

Карта Санкт-Петербурга с более чем 1 300 объектами культурного наследия
Карта Санкт-Петербурга с более чем 1 300 объектами культурного наследия

Где же мистика?

Собрав все правки, пожелания и рекомендации из комментариев я еще раз переделал расчёты:

  • Убрал из выборки Петродворцовый район

  • Добавил Приморский район

  • Ограничил выборку только объектами имеющими федеральное значение

  • Убедился, что значимые архитекторы города, такие как Бенуа, Растрелли, Трезини и др. включены в выборку

В результате всех этих коррекций точка сместилась на адрес Россия, Санкт-Петербург, Инженерная улица, 2-4А, тот самый мистический Инженерный замок, с его призраком, легендами и загадочной надписью на фасаде. Ну чем не мистика?

Ниже обновленная финальная визуализация со всеми коррекциями и новой центральной точкой:

Карта Санкт-Петербурга с более чем 800 объектами культурного наследия федерального значения
Карта Санкт-Петербурга с более чем 800 объектами культурного наследия федерального значения

Если захотите проверить мои расчёты, можете найти все файлы в моём репозитории Github.

Tags:
Hubs:
Total votes 10: ↑7 and ↓3+7
Comments20

Articles