«Зачем мне SQL и python?» — задают резонный вопрос маркетологи или менеджеры по продукту, особенно в сфере недвижимости, оптовой торговли, услуг для бизнеса: «У нас нет миллионов строк данных, нет логов, мы успешно работаем с несколькими таблицами в excel».
Да действительно, у вас может не быть корпоративного хранилища данных в компании, и основой автоматизации работы с данными является Power query (что сейчас в РФ делать все труднее и труднее). Но у вас точно есть данные, которые вы получаете от смежных отделов, из CRM/CDP, MES, АСУ ТП. Эти данные приходят регулярно в виде файлов, и вы сопоставляете эти данные друг с другом с помощью ВПР, фильтруете воронкой, чистите с помощью "Найти или заменить", делаете сводники с помощью функции Pivot table.
Какие есть серьезные проблемы при работе в Excel файлами, кроме очевидных преимуществ.
Нужно писать на языке VBA, чтобы автоматизировать рутину: например, очищать данные, или приводить их к нужному виду. Или вам приходится заказывать эти скрипты в it отделе, а они почему то не спешат вам помочь.
Трудно работать с версиями, делать бэкапы. Код макроса может внезапно перестать работать, а на наладку может уйти много времени.
Нужна лицензия на полноценную работу, есть привязка к Windows, что сейчас крайне актуально для многих компаний в РФ с переходом на Linux. А python и СУБД (Например SQLite или PostgreSQL) имеют открытый исходный код и распространяются свободно.
Код макроса может перестать работать после обновления Excel.
Ограничение excel по размеру в миллион строк, и зависание при работе на больших массивах (более 200 000 строк на средних офисных машинах)
Все эти проблемы решает простая СУБД. Вам даже не нужно поднимать никакой сервер. А вся работа будет построена на простом языке запросов к этой СУБД — SQL, которым можно овладеть на серьезном уровне за 2-3 месяца тренировок. Python для нас будет вспомогательным инструментом, для обработки данных.
В этой первой моей статье на Хабре, я буду делать минимум прямых сопоставлений SQL vs Excel и просто покажу как это может работать. К делу!
Задача
Имея данные от поставщика, о продаже квартир в новостройках в формате csv, маркетологу на их основе, нужно ответить на вопросы о том, в каком районе СПб лучше строить новый ЖК, где был наибольший рост цен и что на него влияет? Как росла или падала недвижимость относительно других активов?

Кроме этого, каждый месяц будут приходить новые данные о сделках, и нужно будет обновлять наш отчет. Что мы сделаем для решения задачи сейчас, и ускорения работы в будущем:
Загрузим файлы в Python, загрузим нужные для работы модули.
Очистим данные от мусора (запятые, неверный тип данных, некорректные даты)
Посчитаем расстояние до центра Питера до каждого дома за 30 секунд
За 1 минуту создадим СУБД и подключимся к ней
Создадим расчетные поля в SQL
Соединим 5 таблиц в одну.
Сделаем график и сводную таблицу
Где мы будем работать? Самый простой способ – это начать работать в VSCode, предварительно установив anaconda. При запуске VSСode выберите "New file", а затем "Jupiter Notebook".

В начале нам нужно импортировать модули, также их еще называют библиотеки. Это уже созданные другими программистами "куски" готового кода. Модуль Geopy
требует установки через командную строку pip install geopy
в магазине с открытым исходным кодом conda/anaconda его нет, поэтому устанавливаем через через pip
.
import pandas as pd #работа с данными
import sqlite3 #СУБД для создания локальной БД. Скоро это будет duckBD:)
from geopy.distance import geodesic #вычисление расстояний по координатам
import matplotlib as plt #графики
import seaborn as sns #графики
Загружаем файлы для работы
Работа в Jupyter Notebook выглядит как ввод команды в ячейке на языке Python и получение результата на выводе или в output

Загрузить файлы в документ можно командой read_csv
, указав адрес, где хранится этот файл. Вы можете указывать и ссылку в интернете или сетевое хранилище. Pandas может читать другие типы файлов, например дамп sql, json, spss, sas.
df = pd.read_csv("/Users/andrew/Documents/DataSets/Dataflat_Oct_2023_LO/realty_sold_07112023_LO.csv", sep = ';')
df #выводим результат

Для наглядности в дальнейшем таблица будет крупнее, и мы не будем выводить все колонки. Пока вы можете видеть объект DataFrame
библиотеки Pandas. Это многомерный массив, или проще привычная нам таблица со строками или столбцами.

Введем команду для получения количества строк в DataFrame

Когда колонок в таблице очень много, вы увидите только 20 колонок. В опциях Pandas вы можете изменить параметры отображения, чтобы у вас появился скролл влево, и отобразились все столбцы.
pd.options.display.float_format = '{:.2f}'.format
# Максимальное число отображаемых колонок.
pd.set_option('display.max_columns', 500)
# Максимальное число отображаемых строк.
pd.set_option('display.max_rows', 500)
Преобразование и очистка данных
Мы загружали данные из csv файла, довольно распространенного формата в корпоративной среде, и наверное вас не обрадует, что в отличии от excel, Pandas не старается угадать тип данных при загрузке файла (строка, число, дата). Типы данных нужно указывать явно, но этого того стоит, так сделаем мы это однажды, и код будет делать нужные манипуляции многократно, освобождая ценное время.
Проверим какие типы данных присвоились столбцам при загрузке.

В первую очередь нас интересуют даты, все они прочитались как object
. Для преобразования типов применим метод to_datetime.
df['Дата ДДУ'] = pd.to_datetime(df['Дата ДДУ'], format='%d/%m/%y')
df['Дата регистрации'] = pd.to_datetime(df['Дата регистрации'], format='%d/%m/%y')
df['Дата регистрации модель'] = pd.to_datetime(df['Дата регистрации модель'], format='%d/%m/%y')
df['Дата обременения'] = pd.to_datetime(df['Дата обременения'], format='%d/%m/%y')
Задать формат даты при импорте файла
Кроме описанных выше способов, есть еще один: задать дату сразу при импорте. Нужно указать параметр parse_dates
pd.read_csv('file_name.csv', parse_dates=['Deal_date'])
Далее мы видим, что некоторые числа, также прочитались программой, как строки или object
. Все это произошло потому, что целую часть от дробной части, у нас разделяет запятая, а не точка. Напишем короткий код, который меняет запятую на точку и переводит строку в числовой тип.
df['Площадь'] = df['Площадь'].str.replace(',', '.').astype(float)
df['lat'] = df['lat'].str.replace(',', '.').astype(float)
df['lng'] = df['lng'].str.replace(',', '.').astype(float)
df['Цена кв. м'] = df['Цена кв. м'].str.replace(',', '.').astype(float)
В анализе данных не обязательно использовать принцип DRY (Не повторяй свой код). По желанию вы можете использовать конструкции языка Python, циклы, функции, но в данной статье используются простые и понятные примеры встроенных методов.
Обрабатываем "Срок сдачи". В исходной таблице было "2 кв 2017", извлекаем цифру с 4 знаками - год с помощью регулярного выражения.

df['Год сдачи'] = df['Срок сдачи'].str.extract(r'(\d{4})') #Выделяем год из строки
df['Год сдачи'] = pd.to_datetime(df['Год сдачи']) #присваевам тип даты
Обрабатываем дату старта продаж. В исходной таблице было "2020.2" тип float. Делим до точки беря первую часть числа. Предварительно приведя число float к строке.

df['Старт продаж К'] = df['Старт продаж К'].astype(str)
df['Старт продаж К'] = df['Старт продаж К'].astype(str).str.split('.').str[0]
В наборе данных полная цена сделки у нас оказалась пустой, поэтому мы решили вычислить ее из площади и цены квадратного метра.
#создаем расчетный столбец и добавляем его в датасет
df['Цена производная'] = df['Цена кв. м'] * df['Площадь']
В наборе данных, с котором нам нужно работать названия столбцов на русском, что будет не так удобно при написании SQL запросов. Это не обязательно, но мы переназовем столбцы на латинице.
Скрытый код
new_column_names = {
'ID ЖК': 'id_zhk',
'ЖК рус': 'zhk_rus',
'ЖК англ': 'zhk_angl',
'Район Город': 'rayon_gorod',
'Округ Направление': 'okrug_napravleniye',
'Регион': 'region',
'АТД': 'atd',
'Застройщик ЖК': 'zastroishchik_zhk',
'Площадь': 'ploshchad',
'Комнатность': 'komnatnost',
'Тип Комнатности': 'tip_komnatnosti',
'Этаж': 'etazh',
'Номер': 'nomer',
'Тип помещения': 'tip_pomeshcheniya',
'Корпус': 'korpus',
'Дата регистрации': 'data_registratsii',
'Условия обременения': 'usloviya_obremeneniya',
'Номер обременения': 'nomer_obremeneniya',
'Дата обременения': 'data_obremeneniya',
'Залогодержатель': 'zalogkhozhatel',
'Длительность обременения': 'dlitel_obremeneniya',
'Тип обременения': 'tip_obremeneniya',
'Оценка цены': 'otsenka_tseny',
'Дата ДДУ': 'data_ddu',
'Уступка': 'ustupka',
'Купил лотов в ЖК': 'kupil_lotov_v_zhk',
'ID Корпус': 'id_korpus',
'класс': 'klass',
'Срок сдачи': 'srok_sdachi',
'Стадия строительства': 'stadiya_stroitelstva',
'lat': 'lat',
'lng': 'lng',
'Ипотека': 'ipoteka',
'Секция': 'sektsiya',
'Отделка': 'otdelka',
'Старт продаж К': 'start_prodazh_k',
'Продавец ЮЛ': 'prodavets_yul',
'Зона': 'zona',
'Купил кв и ап в ЖК': 'kupil_kv_i_ap_v_zhk',
'Стадия строительства в дату ДДУ': 'stadiya_stroitelstva_v_data_ddu',
'Цена ДДУ': 'tsena_ddu',
'Цена со скидкой': 'tsena_so_skidkoy',
'Тип сделки': 'tip_sdelki',
'Разница дат': 'raznitsa_dat',
'Дата регистрации модель': 'data_registratsii_model',
'version': 'version',
'Участок': 'uchastok',
'ФИО': 'fio',
'Цена кв. м': 'price_m2',
'ID дом.рф': 'id_dom_rf',
'Оценка по ЕИСЖС': 'otsenka_po_eiszhzs',
'Unnamed: 51': 'unnamed_51',
'Год сдачи': 'god_sdachi',
'Цена производная': 'price'
}
df = df.rename(columns=new_column_names)
Вычислим расстояние для Эрмитажа
Это нужно для понимания того, как цена меняется в зависимости от центра притяжения в туристическом городе. В нашем файле есть 2 столбца с широтой и долготой объекта недвижимости. Этот показатель правильно применить только к данным в таблице ЖК, а не к сделкам в них (чтобы не занимать постоянную память), но для упрощения сделаем это и там и там.
# координаты Эрмитажа
a = (59.939442, 30.314131)
#применение лямбда функции, которая считает расстояние по координатам
df.apply(lambda row: geodesic(a, (row['lat'], row['lng'])).km, axis=1)

Импорт исторических данных о курсе доллара, золота, Мосбиржи и нефти.
Я повторил все те же операции и с другими файлами с финансовыми данными. Для экономии места в статье, и вашего времени, покажу лишь скриншот загрузки, так как весь код аналогичен заливке данных по сделкам. Была та же самая очистка данных и приведение к нужным типам.

Итак, мы загрузили 6 таблиц, запомните переменные, ниже они будут нужны для названия таблиц в базе данных:
Таблица сделок в переменной
df
Таблица жилых комплексов, где продавались эти квартиры. Переменная
zk
Таблицы с финансовыми данными из 4 источников в соответствующих переменных, например
gold
Создаем базу данных в три строчки
На этом весь почти весь python закончился, он был нужен для того, чтобы получить данные из csv, очистить их, переименовать и добавить туда расстояние до Эрмитажа. Вы можете обогащать свои данные с помощью платных библиотек на подобие dadata
, применять свои модели машинного обучения для классификации клиентов или прогноза оттока клиентов.
Наша цель сейчас – начать писать запросы к данным на языке SQL, который в отличии от питона, проще в освоении диджитал специалистами, финансовыми или торговыми менеджерами.
В данном примере, мы будем создавать очень удобную безсерверную базу данных SQlite. Однако вы можете написать в этом же файле и подключение к любой другой баз данных, которая есть в вашей компании.
con = sqlite3.connect('spb_estate')
cur = con.cursor()
to_sql отправляет dataframe в таблицы базы данных:
zk.to_sql('zk',con,index=False,if_exists='replace') #таблица ЖК
df.to_sql('df',con,index=False,if_exists='replace') #таблица сделок
ind.to_sql('ind',con,index=False,if_exists='replace') #индексы мосбиржи с 2013
q.to_sql('q',con,index=False,if_exists='replace') #курсы доллара с 2013
oil.to_sql('oil',con,index=False,if_exists='replace') #курсы на brent
metals.to_sql('metals', con, index=False, if_exists='replace') #курсы на золото
Jupyter notebook или DBeaver для работы SQL
Где и как работать с базой данных – вопрос удобства и привычки. Выше мы в директории (папке проекта где лежит скрипт Python) создали фаил базы данных SQLite. Мы можем получить доступ к таблицам из программы-клиента DBeaver.
Скачайте DBeaver
Нажмите на кнопку подключения и выберете SQLite
Для работы вам потребуются драйвера, "бобер" скачает их автоматически
Нажмите на "SQL script" и выполните с
Слева у нас будут наши таблицы, которые мы создали выше. Справа - интерфейс для написания и исполнения SQL команд.

SQL в Python
Если вы не хотите из коробки подсветку синтаксиса, схемы отношений таблиц, подсказок, а хотите хардкора, то можете продолжить анализ данных в интерактивной среде Python, используя SQL. Мы уже создали базу данных, остается только передавать ей команды.
Для удобства аналитика, создается функция. На вход она принимает запрос в виде строки и нам возвращается результат запроса к базе данных.
def select(sql):
return pd.read_sql(sql,con)
Никакой магии в этом нет, просто как я писал выше Pandas
может читать и файлы sql. В переменно sql
"зашита" строка запроса на языке SQL, а переменной conn присвоен адрес подключения. (см выше con = sqlite3.connect('spb_estate')
)
#Пандас, прочитай sql фаил, по запросу, по подключению в переменной con
pd.read_sql('SELECT avg(price) FROM df', con)
pd.read_sql('Запрос к базе данных в строке', Подключение, которое мы делали ранее)
Посмотрим, как залились таблицы БД. Делаем запрос к базе данных, получаем список таблиц которые есть в нашей базе данных. Вот как это выглядит:
sql = '''select name from sqlite_master where type = 'table';''' #запрос
select(sql) #вывод результата

И вот они наши 6 таблиц. Можно спокойно работать над анализом данных при помощи sql
Пример анализа данных с помощью языка SQL
Все примеры SQL запросов, будут приведены с подстветкой синтаксиса.
Сделаем первый select
, который поможет нам понять временные границы данных. Получим самые минимальные и максимальные значения.
select --достань мне следующие данные:
max(data_registratsii), -- максимальная дата
min(data_registratsii), -- минимальная дата
min(price), max(price),
min(price_m2),
max(price_m2),
min(ploshchad),
max(ploshchad)
from df -- из какой таблицы?
where data_registratsii not null -- где дата не пустая
and price not null -- и цена не пустая
and -- и
tip_pomeshcheniya = 'квартира'
and price != 0.00 -- цена на кватиры не 0

Построим сводную таблицу, сгруппируем все сделки по годам от 2014 до 2023 в одном запросе, в колонках выведем нужные показатели.
Select --достань мне следующие данные:
-- приводим дату к началу года
date(data_registratsii, 'start of year') as date_year
, count(*) -- считаем количество строк
, avg(price)
, avg(price_m2) -- считаем среднее по площади сделки
, avg(ipoteka)
, avg(ploshchad)
, min(ploshchad)
, avg(dlitel_obremeneniya)
from df -- указываем откуда брать данные
where data_registratsii not null
and price not null
and tip_pomeshcheniya = 'квартира'
and price != 0.00 -- фильруем запрос
-- группируем по дате все данные
group by date(data_registratsii, 'start of year')

Мы увидели, что до 2016 года сделок крайне мало и брать их в анализ не стоит. Анализируем только данные о сделках на первичном рынке с 2016 по 2023 год.
У нас очень много данных, более 200 000 строк сделок, и нам нужно понять как внутри этого набора распределены цены. Давайте разобъем их на процентили и посмотрим на средние цены внутри этих групп. Создадим "корзины" с помощью оконной функции ntile
, которая каждой строке в датасете вернет значение от 1 до 10.
with cte as (
-- В начале нашли процентиль для каждой сделки и другие параметры
SELECT
price,
price_m2,
ploshchad,
data_registratsii,
NTILE(10) OVER (ORDER BY ploshchad) as percentile
FROM df
where data_registratsii not null
and price not null and tip_pomeshcheniya = 'квартира'
)
-- Потом на основе первой таблицы, сделали группировку по percentile
select
percentile,
min(ploshchad),
max(ploshchad),
avg(price),
avg(price_m2),
count(*) from cte
group by 1 -- группировка по первому столбцу в select
SQLite с помощью оконной ранжирующей функции ntile
разбил весь набор данных на 10 равных частей по площади. Это можно использовать для сегментации рынка, при сравнении своих цен с ценами конкурентов.

Соединяем таблицы с помощью join
А теперь добавим финансовые данные. Какой был курс доллара на дату регистрации сделки? Мы в основной таблице сделок добавим по ключу даты другие таблицы. В excel этому есть аналог – функция ВПР.
select
id_zhk,
data_registratsii,
zhk_rus,
price,
ipoteka,
ploshchad,
price_m2 as rub_m2_price,
price_m2 / Quote as m2_dollar_price,
price_m2 / Quote / oil.close as m2_oil_price,
price_m2 / Quote / gold as m2_gold_price,
price_m2 / OPEN as mosex_m2_pice
from df
-- добавим данные по доллару на дату
join q on q.date = df.data_registratsii
-- добавим данные по нефти на дату
join oil on oil.date = df.data_registratsii
-- добавим данные по цене золота
join metals on metals.date = df.data_registratsii
-- добавим данные по цене индексов
join ind on ind.TRADEDATE = df.data_registratsii
where data_registratsii not null
and price not null
and tip_pomeshcheniya = 'квартира'
and price != 0.00
and data_registratsii > '2015.01.01'
and name = 'Индекс МосБиржи'
order by date(data_registratsii)

Давайте узнаем какой процент квартир был продан в ипотеку в том или ином году, проследим тенденцию на рынке.
select
date(data_registratsii, 'start of month') as date_m,
avg(ipoteka) as ipoteka
from df
join q on q.date = df.data_registratsii
join oil on oil.date = df.data_registratsii
join metals on metals.date = df.data_registratsii
where data_registratsii not null
and price not null
and tip_pomeshcheniya = 'квартира'
and price != 0.00
and data_registratsii > '2015.01.01'
group by 1
order by 1 asc
Из полученной таблицы уже в python сделаем график с помощью библиотеки seaborn
и matplotlib
plt.figure(figsize=(20, 6)) #размер графика
sns.lineplot(x='date_m', y='ipoteka', data=select(sql)) #оси и dataframe
sns.set_style("darkgrid") #стиль
plt.title('Доля сделок в ипотеку') #заголовок
plt.xticks(rotation=90) #наклон дат
plt.show() #показать график



А теперь сгруппируем наши данные по расстоянию от Эрмитажа в километрах. Также мы в столбцах хотим видеть и динамику в годах, для этого нужно будет делать условные столбцы, с помощью логической конструкции case when, которая дословно говорит "Если дата регистрации между 2017 и 2018 годом, то создай столбец с значениями этого года". Это одна из моих любимы и частых конструкций.
select
round(distance_hermitage, -2),
count(case when data_registratsii between '2016-01-01' and '2017-01-01' then data_registratsii end) as s2016,
count(case when data_registratsii between '2017-01-01' and '2018-01-01' then data_registratsii end) as s2017,
count(case when data_registratsii between '2018-01-01' and '2019-01-01' then data_registratsii end) as s2018,
count(case when data_registratsii between '2019-01-01' and '2020-01-01' then data_registratsii end) as s2019,
count(case when data_registratsii between '2020-01-01' and '2021-01-01' then data_registratsii end) as s2020,
count(case when data_registratsii between '2021-01-01' and '2022-01-01' then data_registratsii end) as s2021,
count(case when data_registratsii between '2022-01-01' and '2023-01-01' then data_registratsii end) as s2022,
count(case when data_registratsii between '2023-01-01' and '2024-01-01' then data_registratsii end) as s2023
from df
join q on q.date = df.data_registratsii
join oil on oil.date = df.data_registratsii
join metals on metals.date = df.data_registratsii
where data_registratsii not null and price not null and tip_pomeshcheniya = 'квартира' and price != 0.00 and data_registratsii > '2015.01.01'
group by 1
order by 1 asc
limit 20
Для полученной таблицы сделаем тепловую карту dataframe
(как условное форматирование в excel), применим условное форматирование ячеек в dataframe
. Возможно застройщику, такие таблицы помогут определиться с эффективной ценой и удаленностью локации застройки от исторического центра СПб.
select(sql).style.background_gradient(axis=None, vmin=1, vmax=10000)

В каком ЖК больше всего сделок в период с 1 января 2020 года? Вы уже сами можете без труда прочитать запрос.
select
df.zhk_rus,
okrug_napravleniye,
rayon_gorod,
count(data_registratsii) as cnt_deal,
avg(price_m2) as rub_m2_price,
avg(price_m2) / Quote as m2_dollar_price,
avg(ipoteka) as ipoteka_precent,
max(date(god_sdachi)) as max_god_sdachi,
min(distance_hermitage)
from df
join q on q.date = df.data_registratsii
where data_registratsii not null and price not null
and tip_pomeshcheniya = 'квартира' and price != 0.00
and data_registratsii > '2020.01.01'
group by 1,2 -- групируем по 1 и 2 столбцу в select
order by 4 desc -- сортируем по убывани по 4 стобцу в select

Какая тройка лидеров в ипотечном кредитовании с 2016 года по количеству сделок?
select
zalogkhozhatel,
count(data_registratsii) as cnt_deal,
sum(price),
avg(dlitel_obremeneniya),
avg(price_m2) as rub_m2_price
from df
join q on q.date = df.data_registratsii
join oil on oil.date = df.data_registratsii
join metals on metals.date = df.data_registratsii
where data_registratsii not null and price not null and tip_pomeshcheniya = 'квартира' and price != 0.00 and data_registratsii > '2016.01.01'
group by 1
order by 2 desc
limit 10

plt.figure(figsize=(20, 8))
sns.barplot(x='zalogkhozhatel', y='cnt_deal', data=select(sql))
sns.set_style("darkgrid")
plt.title('Количество сделок с обременением на первичном рынке с 2016 года по третий квартал 2023 года в Спб')
plt.xticks(rotation=90)
plt.show()

Вместо заключения
Если у вас в компании есть типовой процесс работы с данными, например анализ цен конкурентов, товарных запасов и продаж, то запросто сами или с помощью аналитика сможете организовать такой процесс обработки и визуализации данных с помощью кода.
Вам не придется возиться с excel-таблицами и каждый раз переделывать одни и те же манипуляции, вы просто можете пропустить данные через подобный скрипт, который готовит нужные вам отчеты!
P.S.
Используемый датасет является коммерческим и отсутствует в открытом доступе, поэтому выложен быть не может.