Senior Data Analyst
780 subscribers
337 photos
9 videos
157 links
Data Analyst 360°
SQL, Python, ML, GPT, матстат и визуализация
BI, A/B, метрики, продуктовая аналитика
Для junior → middle → senior

Если хотите поддержать автора — здесь можно это сделать https://teletype.in/@seniorru

По вопросам: @seniorru
Download Telegram
Продолжение следует....
👍8
🧠 Python для аналитиков: if, elif, else — без боли
Когда «управление логикой» звучит не как философия, а как реальный навык

Это первая серия модуля 3 — и она про то, как писать условия, не превращаясь в программиста 2008 года.

Что внутри:
→ на пальцах объясняем, как работает if, elif, else
→ 10 примеров с комментариями: от “число положительное?” до “если всё сложно, делай красиво
→ практическое задание: определяем возрастную категорию
→ советы и фейлы: как не закопаться в условиях и не получить else: print('что-то пошло не так')
→ реальный кейс: расчёт налога по типу дохода, возрасту и льготам — пригодится даже в жизни

Это не учебник. Это фундамент.
После этой серии if перестанет быть пугающим, а начнёт работать на вас.

💸 Поддержать канал и скачать серию можно здесь
📣 И да, поставь реакцию за старание

👉 Senior Data Analyst | #python #модуль_03 #серия_01
🔥65
🧠 Как писать свои SQL‑функции в PostgreSQL — и зачем это нужно?

Собрали прод‑разбор:
– scalar, plpgsql и TVF функции с реальными кейсами;
– зачем нужны IMMUTABLE и STABLE;
– как вынести бизнес‑логику в UDF и перестать дублировать CASE‑ы;
– примеры для сегментации, нормализации, топ‑N.

Сделайте свои витрины легче, код — чище, и отчёты — стабильнее.

💸 Поддержать канал
👉 Senior Data Analyst | #sql
👍9
🧠 Зачем вообще SQL-функции, если можно просто написать SELECT?

На ранних этапах всё просто: CASE WHEN, пара JOIN-ов, и ты доволен.
Но с ростом проекта — и твоим опытом — появляется ощущение, что код начинает душить.


Когда проект растёт, вместе с ним растёт и боль:
• дублирующаяся логика
• громоздкие CASE WHEN
• правила, разбросанные по десяткам CTE
• метрика меняется — ты бегаешь по скриптам с огнетушителем

Вот тут и заходят на сцену SQL-функции (UDF).
Они позволяют вынести логику в одно место. Изолированно. Переиспользуемо. Как функции в коде, только внутри БД.

▪️ Какие бывают функции в PostgreSQL?
• Скалярные
normalize(score, avg, sd) → возвращают одно значение
Используются в метриках, флагах, категоризациях

• Табличные
top_n_per_category(n) → возвращают таблицу
Удобны для создания витрин и кастомных выборок с параметрами

• Агрегатные
my_median(val) → агрегируют значения
Нужны, если стандартных агрегатов мало или не подходят


▪️Реальные кейсы
1️⃣ Категоризация пользователей
Было: CASE WHEN spend > 10000 AND visits > 5 THEN 'VIP' ...
Стало: customer_segment(spend, visits)
→ читается, тестируется, правится в одном месте

2️⃣ Флаг аномалии
is_outlier(val, avg, stddev)

→ можно использовать в WHERE, CASE, JOIN, даже в dbt-тестах

3️⃣ Параметризуемая витрина
SELECT * FROM top_n_per_category(3)

→ больше не пишешь ROW_NUMBER вручную в каждом скрипте

▪️IMMUTABLE, STABLE, VOLATILE — важнее, чем кажется
IMMUTABLE — всегда даёт одинаковый результат при одних и тех же аргументах
→ используется для расчётов (abs(x), normalize(...))

STABLE — может читать таблицы, но не меняет данные
→ подходит для SELECT ... WHERE id = x

VOLATILE — может вести себя непредсказуемо
random(), now(), count(*)

Если не указать тип, PostgreSQL подставит VOLATILE, и оптимизации не будет. А это уже тормоза.


▪️Тестирование UDF
Функции можно (и нужно) тестировать:
• pgTAP — юнит-тесты прямо в PostgreSQL
• ручные тесты через begin...end
• автотесты через CI: пушнул код — сравнил результат
SELECT customer_segment(11000, 6) = 'VIP' AS test_1;


▪️Как устроено в проде
1. Версионирование
segment_v1, segment_v2, segment_active
→ переключаешь alias, не ломаешь пайплайн

2. Интеграция с dbt и Airflow
→ в dbt — используешь как обычную функцию
→ в Airflow — вызываешь в PostgresOperator

3. Документация
COMMENT ON FUNCTION внутри тела
→ changelog логики — в доках проекта


▪️Типичные ошибки

• писать скалярную функцию, которая делает SELECT
→ это отдельный запрос на каждую строку → тормозит

• не указывать IMMUTABLE или STABLE
→ теряется кеш, растёт нагрузка

• изменять функции без версионирования
→ одно изменение — и всё падает

• писать гигантские UDF без профилирования
→ сложно читать, плохо оптимизируется

▪️Вывод

SQL-функции — про стабильность, переиспользуемость и контроль.

Они дают тебе одно: уверенность, что логика работает так, как ты задумал. И что её не нужно чинить каждую неделю.

💸 Поддержать канал
👉 Senior Data Analyst | #sql
8👍1
🧠 Погнали разогревать мозги — задачи ждут!

Если и есть день для чётких решений — это сегодня. Среда, энергия есть, мозг включён!


💸 Поддержать канал
👉 Senior Data Analyst | #матстат #теорвер
🔥6
🔵 Задача №1: Среднее или медиана?
Математическая статистика


Доходы в выборке (в рублях):
40 000, 42 000, 43 000, 44 000, 2 000 000

Какую метрику лучше использовать — среднее или медиану? Почему?

Разбор:
Среднее = (40к + 42к + 43к + 44к + 2млн)/5 ≈ 442 000
Медиана = 43 000


Ответ: медиану

Среднее сильно искажено выбросом (2 млн). В таких случаях для описания "типичного" дохода лучше использовать медиану.
Это основа устойчивости статистик — всегда проверяй выбросы перед расчетом среднего!



🔵 Задача №2: Доверие к результату
Математическая статистика


Вы провели A/B-тест:
• Группа A: 5000 юзеров, конверсия — 5%
• Группа B: 100 юзеров, конверсия — 6%

Можно ли сказать, что B лучше?

Разбор:
Повышение есть (5% → 6%), но выборка в B слишком мала, чтобы делать вывод.

Ответ: Нет, нельзя
Для вывода нужна статистическая значимость. При малом размере выборки возможна случайная флуктуация.
Хорошая практика: сначала убедись, что выборка достаточна, потом — смотри на конверсии и p-value.



🔴 Задача №3 : Монетка и игральный кубик
Теория вероятностей


Сначала подбрасываем честную монету.
Если выпал орёл — бросаем кубик один раз.
Если решка — бросаем кубик дважды и берём сумму.

Какова вероятность, что в итоге получим число 6?

Разбор:
Делим на 2 сценария:
1. Орёл → 1 бросок → P(выпадет 6) = 1/6
2.Решка → 2 броска → сколько комбинаций даёт сумму 6?

Варианты:
(1,5), (2,4), (3,3), (4,2), (5,1) — 5 из 36

Общий ответ:
P = 0.5 × (1/6) + 0.5 × (5/36) = (3/36) + (5/72) = 0.2083… ≈ 20.8%


Ответ: ≈ 20.8%
Тут важна комбинация условной и полной вероятности, учитывая разные сценарии.


🔴 Задача №4 : День рождения в команде
Теория вероятностей


В команде из 23 человек — какова вероятность, что у хотя бы двух день рождения в один день?
(Принять 365 дней в году, игнорируем високосные)

Разбор:
Эта задача известна как парадокс дня рождения.
Вместо прямого счёта — удобнее посчитать обратное событие:
P(все разные) ≈ 0.493
365/365 × 364/365 × 363/365 × ... × 343/365 ≈ 0.4927
Шанс, что все 23 дня рождения разные → 49%


Но, нас-то интересует обратное событие —
что хотя бы у кого-то дни рождения совпадут:
→ P(хотя бы совпадение) = 1 – 0.493 = 0.507



Ответ: ≈ 50.7%
Парадокс: всего 23 человека, а шанс совпадения уже больше 50%!
Классический пример, где человеческая интуиция сильно ошибается.
🔥71
🧠 Почему аналитик дважды подумает, прежде чем писать IFNULL, ROUND, CAST, NOW()
и что потом с этим делать, когда всё сломалось

Потому что эти штуки:
— тихо маскируют ошибки (NULL → 0, и привет кривой отчёт)
— ломают агрегации (ROUND в строках → минус доверие к итогам)
— ведут к нестабильности (NOW() в проде ≠ NOW() в dev)
— создают магию, которую потом никто не может воспроизвести

"Лишь бы работало" — не подход.


Вместо встроенных хакающих функций —
нормальные SQL-функции с понятной логикой, версионированием, комментариями и тестами.

В посте:
•  разбор, почему эти функции опасны
•  читаемые примеры
•  как писать надёжный SQL

💸 Поддержать канал
👉 Senior Data Analyst | #sql
👍71
▪️Что не так?
Базовые SQL-функции кажутся удобными.
Но если применять их бездумно — это путь к тихим багам, кривым отчётам и вечному “а почему здесь 0?”

▪️Где сыпется чаще всего:

COALESCE / IFNULL
Выглядит мило: COALESCE(value, 0)
Но скрывает NULL и мешает понять, откуда дыра.
В результате ты "лечишь" витрину, которая уже умерла.

Правильно:
→ Комментируй замену: AS logic_zero_substitute
→ Лучше — is_data_missing как отдельная колонка

Кейс: retention по оплатам. Пользователь не платил → NULL → подставили 0 → метрика выросла. Радоваться рано.

▪️ ROUND(..., 2)
Если ты округляешь до суммы — всё норм.
Если до — всё грустно.

Раннее округление:
→ копится погрешность
→ сумма строк != total
→ отчёт бьётся по швам

Правильно:
→ округляем после агрегации
→ лучше в обёртке типа adjust_for_report(val)

▪️NOW() / CURRENT_TIMESTAMP
Самая коварная “удобная” функция.
Каждый вызов может вернуть разное время.
В prod — ок, в тесте — ничего не сходится.
Airflow, dbt — сбиваются от timezone или минуты запуска.

Правильно:
→ один clock_timestamp() в CTE
→ или передавай дату явно
→ всегда указывай UTC

▪️CAST()
CAST('' AS INT) — NULL
CAST('2024-12-40' AS DATE) — NULL
А ты не в курсе, потому что SQL промолчал.

Правильно:
TRY_CAST, если есть
→ логировать NULL
→ комментировать формат явно

Кейс: отчёт по регистрациям. Столбец с '0000-00-00' → стал NULL → “а почему у нас 300 юзеров без даты?”

▪️ Как бы думал я
•  Junior: “лишь бы работало”
•  Senior: “чтобы было читаемо, проверяемо и поддерживаемо”

Вместо встроенных функций — оборачивай бизнес-логику в UDF:
adjusted_spend(spend, rate)

→ видно, что происходит
→ можно покрыть тестами
→ можно писать changelog

▪️ Ну и куда, без тестов
•  pgTAP или ручные проверки
•  edge-cases: NULL, пустые строки, нули, текст
•  COMMENT ON FUNCTION, changelog, CI с EXPLAIN ANALYZE

▪️ Кейс: баг, который "починили", а стало хуже
Ситуация: отчёт по выручке. Где-то в исходных данных revenue = NULL, потому что JOIN не сработал.

Аналитик пишет:
SELECT COALESCE(revenue, 0) AS revenue_cleaned

Отчёт выходит нормальным. Все рады. Только потом выясняется:
эти пользователи вообще не должны были попасть в выборку.
0 вместо NULL скрыл баг в логике JOIN, и теперь все графики — фальшивка.

Как нужно было сделать:

1. Заменил бы NULL — и задал вопрос: "почему здесь нет данных?"
2. Временная замена? Только с явным AS zero_if_missing и комментарием.
3. Логика обработки — в отдельную UDF с бизнес-смыслом.
4. Плюс флажок is_data_missing, чтобы не потерять контекст.

Не всякий NULL — враг. Иногда он — единственный сигнал, что что-то пошло не так.

💥 Как тебе ?
🔥 — узнал(а) что-то новое
🤝 — темы хорошо знакомы

💸 Поддержать канал
👉 Senior Data Analyst | #sql
🔥7👍42
🧠 Как ускорять SQL-запросы?
Если ты всё ещё веришь в “индекс спасёт всегда” — у меня для тебя EXPLAIN ANALYZE.

В посте:
– читаем план запроса, не глядя в потолок
– разбираем cost, actual rows, loops
– находим, где JOIN душит, а GROUP BY тормозит
– учимся ускорять без пересоздания БД

EXPLAIN — это рентген твоего SQL.
Хочешь писать запросы, которые не тормозят? Погнали 👇

💸 Поддержать канал
👉 Senior Data Analyst | #sql
🔥3👏3
🛠 Как читать EXPLAIN и ускорять SQL-запросы
Часть 2: практичный гайд, без теории ради теории

▫️Что делает EXPLAIN?
Показывает, как именно PostgreSQL будет исполнять запрос:
что читает, в каком порядке, как соединяет таблицы, сколько строк ждёт и сколько реально вернул.

EXPLAIN ANALYZE — то, что используем в проде:
показывает фактическое выполнение:
•  строки, прошедшие через каждый шаг
•  время выполнения
•  количество повторов (loops)
•  сколько реально читали с диска

▫️Как читать план:
Тип операции:
Seq Scan — чтение всей таблицы. Норм на маленьких, боль на больших
Index Scan — идеал при точном фильтре
Bitmap Index Scan — хорош при нескольких фильтрах
Nested Loop — хорошо при малом внешнем наборе
Hash Join — рулит при JOIN ON a = b и достаточно памяти
Sort — потенциально опасен, если не влезает в память
Читай снизу вверх: от таблиц к джойнам и агрегациям

Rows / Loops / Time
actual time=0.05..0.09 rows=1 loops=1

•  rows — сколько вернул
•  loops — сколько раз вызвали
•  если expected ≠ actual — планировщик ошибается → неоптимальный план

Total cost
Не время, а "оценка тяжести" запроса.
Используй для сравнения разных планов.

BUFFERS
Добавь EXPLAIN (ANALYZE, BUFFERS)
•  read → реально лезли на диск
•  shared hit → всё в кеше
•  если много read → нет индекса, не влезло в память

▫️Как реально ускорять запросы ?

1. Индексы по WHERE, JOIN, GROUP BY
CREATE INDEX idx_orders_user_date ON orders(user_id, order_date);

Порядок колонок важен. Не ставь индексы "на всякий".

2. Делим фильтрацию и агрегацию
Вместо:
SELECT user_id, COUNT(*) FROM orders WHERE ... GROUP BY user_id;

Так:
WITH filtered AS (
SELECT user_id FROM orders WHERE ...
)
SELECT user_id, COUNT(*) FROM filtered GROUP BY user_id;


3. Следи за LEFT JOIN
Фильтр по правой таблице? Лучше вынеси в подзапрос.
PostgreSQL может выбрать ужасный план.
•  pg_stat_statements — какие запросы дорогие
•  pg_stat_user_indexes — какие индексы реально нужны
•  auto_explain — лог запросов с плохими планами
•  track_io_timing — кто грузит диск

▫️Типичные ошибки:
• Seq Scan по большой таблице — нет фильтра или индекса
• JOIN по неиндексированному полю → Hash Join на всё
• Группировка по "широкой" колонке → сортировка в памяти
• Подзапрос без LIMIT — тянет всё
• ORDER BY без индекса — внешняя сортировка, тормоза

▫️Как нужно оптимизировать
•  EXPLAIN ANALYZE — найти узкое место
•  Сформулировать гипотезу (индекс, переписать логику)
•  Внести минимальные изменения
•  Проверить новый план
•  Повторять, пока не будет норм

Важно не просто “сделать быстрее”, а понимать — почему стало быстрее.


💥 Как тебе ?

🔥 — узнал(а) что-то новое
🤝 — темы хорошо знакомы

💸 Поддержать канал
👉 Senior Data Analyst | #sql
🔥6🤝3
Так можно визуализировать разницу между группами — понятно, гибко и без BI.

Bar-график с доверительными интервалами — для простого сравнения средних.
Boxplot со swarm — чтобы увидеть выбросы и структуру распределения.
KDE — когда хочется заглянуть в форму данных: есть ли смещение, мода, хвосты.

Чистый код, никаких кликов, полная автоматизация. Подходит, если ты работаешь в Jupyter, хочешь встроить график в пайплайн или просто устал от BI-интерфейсов.

💸 Поддержать канал
👉 Senior Data Analyst | #Python
🔥5👌2