вторник, 30 апреля 2024 г.

Some features of window functions

Определение окна может быть сохранено для повторного использования так:
SELECT *,
       ROW_NUMBER() OVER my_window,
       RANK() OVER my_window,
       DENSE_RANK() OVER my_window
FROM online_store.orders
WINDOW my_window AS (ORDER BY revenue); 

-- конструкцию WINDOW указывают после оператора WHERE и до оператора ORDER BY
Можно записать сразу несколько определений окна

WHERE ...
WINDOW my_window AS (ORDER BY revenue),
       one_more_window AS (PARTITION BY user_id)
ORDER BY ...
Что нельзя:
У оконных функций есть ограничения, которые нужно учитывать.
 
  • использовать DISTINCT в аргументе оконной функции, например COUNT(DISTINCT поле)
  • оконные функции нельзя сочетать с группировкой GROUP BY
  • оконные функции нельзя использовать в условиях после WHERE, как и агрегирующие функции (Вычисление агрегирующих и оконных функций происходит после срезов)
 однако такие запросы сработают


SELECT DISTINCT 
             event_dt,
       COUNT(*) OVER (PARTITION BY event_dt)
FROM online_store.orders;

-- Похожую итоговую таблицу выдаст запрос с GROUP BY
SELECT DISTINCT 
             event_dt,
       COUNT(*)
FROM online_store.orders
GROUP BY event_dt;
полный список оконных функций - https://postgrespro.ru/docs/postgrespro/12/functions-window

Offset window functions

Кумулятивные значения рассчитываются с накоплением. Простой пример — счёт. Оценивая количество, вы называете числа: один, два, три и т. д. Каждое следующее число вы получаете, прибавляя единицу.
SELECT *,
       SUM(costs) OVER (ORDER BY dt) AS costs_cum
FROM online_store.costs
WHERE channel = 'Yandex';
Запрос отсортировал данные в поле dt по возрастанию — это поле указано после оператора ORDER BY в выражении OVER. Сумма с накоплением считается так: к значению предыдущей записи прибавляется значение текущей — и так с каждой записью до конца таблицы. Сумму с накоплением можно рассчитать и для каждого окна отдельно. Для этого нужно добавить оператор PARTITION BY и указать нужное поле.
SELECT user_id,
       session_duration,
       session_start,
       SUM(session_duration) OVER (PARTITION BY user_id ORDER BY session_start)
FROM online_store.sessions;
Кумулятивно посчитаем количество пользователей

SELECT user_id,
       COUNT(*) OVER (PARTITION BY dt ORDER BY user_id)
FROM online_store.profiles;
Это то же что и

-- использовать ROW_NUMBER в данном случае считается лучшим стилем
SELECT user_id,
       ROW_NUMBER() OVER (PARTITION BY dt ORDER BY user_id)
FROM online_store.profiles
Функции MIN, MAX, AVG тоже используют для расчёта кумулятивных значений.

SELECT *,
       MIN(costs) OVER (ORDER BY dt) AS costs_cum
FROM online_store.costs
WHERE channel = 'Yandex';

LEAD(), LAG()

Относятся к функциям смещения. С помощью функций смещения можно для каждой записи с заказом пользователя вернуть дату прошлого или следующего заказа — previous_order_dt и next_order_dt соответственно. Функция LAG() позволяет возвращать предыдущие записи, а LEAD() — следующие.
SELECT user_id,
         event_dt,
       LAG(event_dt) OVER (PARTITION BY user_id ORDER BY event_dt) AS previous_order_dt,
       LEAD(event_dt) OVER (PARTITION BY user_id ORDER BY event_dt) AS next_order_dt
FROM online_store.orders
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY user_id;
У функций есть несколько аргументов:
  • LEAD(поле, смещение, значение по умолчанию) OVER (определение окна)
  • LAG(поле, смещение, значение по умолчанию) OVER (определение окна)

-- 1 и  NULL - можно не указывать это значения по умолчанию
SELECT user_id,
             event_dt,
       LEAD(event_dt, 1, NULL) OVER (PARTITION BY user_id ORDER BY event_dt) AS next_order_dt
FROM online_store.orders 
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY user_id;

  • LEAD() и LAG()  используют аргумент для смещения в типе integer
  • использование отрицательных значений в качестве аргумента инвертирует действие функций
  • аргумент для значения по умолчанию должен быть в том же типе что и поле к которому применяется функция

SELECT user_id,
             event_dt,
       LAG(event_dt, 2, '2020-01-01') OVER (PARTITION BY user_id ORDER BY event_dt) AS previous_order_dt
FROM online_store.orders 
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY user_id;
С помощью функций LEAD() и LAG() удобно рассчитывать время между событиями. Например, можно рассчитать, сколько дней прошло со дня предыдущего заказа пользователя.

SELECT user_id,
           event_dt,
       LAG(event_dt) OVER (PARTITION BY user_id ORDER BY event_dt) AS previous_order_date,
         event_dt - LAG(event_dt) OVER (PARTITION BY user_id ORDER BY event_dt) AS days_from_previous_order
FROM online_store.orders
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY user_id;

-- найдем количество пользователей за каждый день
WITH users_cnt AS (
    SELECT session_start::date AS session_date,
               COUNT(DISTINCT user_id) AS users_cnt
    FROM online_store.sessions
    GROUP BY session_date
)
-- получим пользователей за 7 дней назад
-- и посчитаем во сколько раз увеличилось количество пользователей
SELECT *,
             LAG(users_cnt, 7) OVER (ORDER BY session_date) AS previous_weekday_users_cnt,
             users_cnt::numeric / LAG(users_cnt, 7, users_cnt) OVER (ORDER BY session_date) AS user_growth
FROM users_cnt;
Еще примеры:

SELECT o.order_id,
       o.user_id,
       o.paid_at,
       LAG(o.paid_at, 1, '1980-01-01') OVER(PARTITION BY o.user_id ORDER BY o.paid_at) AS prev_order_dt
FROM tools_shop.orders AS o

SELECT e.event_id,
       e.event_time,
       e.user_id,
       LEAD(e.event_time) OVER(PARTITION BY e.user_id ORDER BY e.event_time)
FROM tools_shop.events AS e;

SELECT e.event_id,
       e.event_time,
       e.user_id,
       LEAD(e.event_time) OVER(PARTITION BY e.user_id ORDER BY e.event_time) - e.event_time
FROM tools_shop.events AS e;

Ranking window functions

ROW_NUMBER() не требует аргумента передаваемого в функцию
SELECT *,
             ROW_NUMBER() OVER ()
FROM online_store.orders 
WHERE user_id IN (300768196,
                  840452722,
                  59432616);
-- ROW_NUMBER нумерует записи в таблице в том порядке в котором они в ней представлены



ORDER BY выполняется после ROW_NUMBER()
SELECT *,
             ROW_NUMBER() OVER ()
FROM online_store.orders 
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY user_id;
ORDER BY можно передать в функцию OVER
SELECT *, 
       ROW_NUMBER() OVER (ORDER BY user_id DESC)
FROM online_store.orders 
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY user_id;
Можно сортировать по нескольким полям

SELECT *, 
       ROW_NUMBER() OVER (ORDER BY user_id, event_dt DESC)
FROM online_store.orders 
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY user_id;
Пример выбора записи с определенным рангом

WITH sessions AS
  (SELECT *,
          ROW_NUMBER() OVER (ORDER BY session_start) AS rn
   FROM online_store.sessions)
SELECT *
FROM sessions
WHERE rn = 100;

RANK() и DENSE_RANK()

отличаются от RAW_NUMBER() тем как происходит ранжирование одинаковых значений:
  • RAW_NUMBER() нумерует последовательно
  • RANK() нумерует одинаковыми значениями одинаковый ранг, но чтобы вычислить каждый следующий ранг, к текущему номеру ранга прибавляется количество записей этого ранга. Поэтому нумерация растет не последовательно
  • DENSE_RANK() нумерует одинаковыми значениями одинаковый ранг, нумерация последовательная
SELECT *, 
       RANK() OVER (ORDER BY user_id)
FROM online_store.orders
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY user_id;
SELECT *, 
       DENSE_RANK() OVER (ORDER BY user_id)
FROM online_store.orders 
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY user_id;

NTILE()

Делит весь объем записей на N бакетов так, чтобы количество записей в группах было максимально близким. Если записи не получается разбить на группы поровну, предпочтение отдаётся первым группам — в них войдёт больше записей. NTILE() принимает на вход количество рангов.
SELECT *,
         NTILE(3) OVER (ORDER BY revenue)
FROM online_store.orders 
WHERE user_id IN (300768196,
                  840452722,
                  59432616)
ORDER BY revenue;



Операторы PARTITION BY и ORDER BY

влияют на результат работы оконной функции. В выражении OVER их можно использовать вместе. Выберем данные о втором по счёту заказе для каждого пользователя. Оператор PARTITION BY позволяет разделить данные на окна по user_id. Оператор ORDER BY отсортирует значения внутри окон по дате заказа, а функция ROW_NUMBER() их проранжирует.

-- данные о втором по счёту заказе для каждого пользователя:
WITH orders AS
  (SELECT *,
          ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_dt) AS rn
   FROM online_store.orders)
SELECT *
FROM orders
WHERE rn = 2;

-- третья с конца сессия для каждого пользователя
WITH sessions AS
  (SELECT *,
          ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY session_start DESC) AS rn
   FROM online_store.sessions)
SELECT *
FROM sessions
WHERE rn = 3;
Функцию NTILE() тоже можно использовать в сочетании с оператором PARTITION BY. В этом случае записи разделятся на группы не по всей таблице, а внутри каждой из групп, сформированных PARTITION BY
SELECT *,
         NTILE(3) OVER (PARTITION BY user_id ORDER BY session_start)
FROM online_store.sessions 
WHERE user_id IN (300768196,
                  840452722,
                  59432616);

Aggregate window functions

They enable users to perform a wide range of analytical tasks, from ranking and partitioning data to calculating moving averages, cumulative sums, and differences between consecutive rows. Оконная функция как и аггрегирующая функция работает с набором записей, но в отличие от нее возвращает не одно значение, а набор (столько же записей сколько получила на вход).
SELECT *,
       AVG(revenue) OVER () AS total_avg
FROM online_store.orders
LIMIT 5;
-- выведет строки исходной таблицы с добавлением столбца с average значением
--
-- нужно разделить записи на разделы-окна по параметрам в выражении OVER
-- затем посчитать среднюю выручку по записям в каждом окне
то же что и
SELECT *,
       (SELECT AVG(revenue) AS total_avg FROM online_store.orders)
FROM online_store.orders
LIMIT 5;
PARTITION BY
SELECT *,
       AVG(revenue) OVER (PARTITION BY event_dt) AS date_avg
FROM online_store.orders;
-- партицируем по дате, для каждой даты считаем среднее revenue
-- дополняем исходную таблицу колонкой date_avg
то же что и

SELECT *
FROM online_store.orders AS ord
LEFT JOIN (SELECT event_dt,
                  AVG(revenue) AS date_avg
           FROM online_store.orders
           GROUP BY event_dt) AS dt ON ord.event_dt = dt.event_dt;


-- по каждому пользователю ищет дату последнего заказа
SELECT *,
       MAX(event_dt) OVER (PARTITION BY user_id) AS last_order_dt
FROM online_store.orders;

-- количество заказов по пользователям
SELECT *,
       COUNT(*) OVER (PARTITION BY user_id) AS orders_cnt
FROM online_store.orders;
Оконные функции бывают:
  • агрегирующие AVG, MIN, MAX, SUM и COUNT
  • функции общего назначения RAW_NUMBER, RANK, DENSE_RANK, NTILE, 
  • функции смещения LEAD, LAG

SELECT *,
       AVG(revenue) OVER (PARTITION BY user_id) AS user_avg
FROM online_store.orders;


Можно партицировать сразу по нескольким полям перечисленным через запятую, тогда в окно войдет каждое уникальное сочетание указанных полей

понедельник, 29 апреля 2024 г.

BI calculations in SQL - examples

CR

SELECT ROUND(COUNT(DISTINCT orders.user_id) * 100.0 / COUNT(DISTINCT users.user_id), 1)
FROM tools_shop.users AS users
LEFT JOIN tools_shop.orders AS orders ON users.user_id = orders.user_id; 

LTV за весь период (ARPPU)

SELECT SUM(revenue) / COUNT(DISTINCT user_id) AS ltv
FROM online_store.orders;
SELECT p.channel,
       SUM(o.revenue) / COUNT(DISTINCT o.user_id) AS ltv
FROM online_store.orders o
JOIN online_store.profiles p ON o.user_id = p.user_id
GROUP BY p.channel;
ARPU
SELECT SUM(o.revenue) / COUNT(DISTINCT p.user_id) AS arpu
FROM online_store.profiles p
LEFT JOIN online_store.orders o ON o.user_id = p.user_id;
SELECT SUM(orders.total_amt) / COUNT(DISTINCT users.user_id) AS ltv
FROM tools_shop.orders AS orders
RIGHT JOIN tools_shop.users AS users ON users.user_id = orders.user_id;
Конверсия пользователей из первого заказа во второй
SELECT COUNT(so.user_id) * 100.0 / COUNT(p.user_id) AS cr
FROM online_store.profiles p
LEFT JOIN
-- пользователи, которые оформили два заказа и более
  (SELECT user_id,
          COUNT(*) AS orders_cnt
   FROM online_store.orders 
   GROUP BY user_id
   HAVING COUNT(*) >= 2) so ON p.user_id = so.user_id
WHERE payer IS TRUE; -- пользователи, которые совершали заказ

ROI Группируем затраты по дням

SELECT dt,
       SUM(costs) AS costs
FROM online_store.costs
GROUP BY dt;
получаем revenue по дням
SELECT event_dt AS dt,
       SUM(revenue) AS revenue
FROM online_store.orders
GROUP BY event_dt;
Соединяем
SELECT dc.dt,
       revenue * 100 / costs AS ROI
FROM
  (SELECT dt,
          SUM(costs) AS costs
   FROM online_store.costs 
   GROUP BY dt) dc
JOIN
  (SELECT event_dt AS dt,
          SUM(revenue) AS revenue
   FROM online_store.orders 
   GROUP BY event_dt) dr ON dc.dt = dr.dt;

пятница, 26 апреля 2024 г.

ME: Labour Force

 https://www.monstat.org/eng/page.php?id=22&pageid=22

Data copy - https://docs.google.com/spreadsheets/d/19QqfNP4NkIrvjP_salnVmpKjjVOv6gZflZ55Iee6oRA/edit?usp=sharing



ME: Annual gross domestic product

 https://www.monstat.org/eng/page.php?id=19&pageid=19

Annual GDP presents market value of all goods and services produced in the country during one year. GDP is compiled by production and expenditure approaches.

Data copy - https://docs.google.com/spreadsheets/d/13Rc3vuQy6_fVtna9CWKEz0j2aBMNSDd8muWbI4pfpUQ/edit?usp=sharing



воскресенье, 21 апреля 2024 г.

Google Ads Video Campaigns notes

Awareness and consideration 
Reach a broad audience and build interest in your products or brand

  • Display Reach customers across 3 million sites and apps with engaging creative
  • Video Reach viewers on YouTube and get conversions
  • Demand Gen Drive demand and conversions on YouTube, Discover and Gmail with image and video ads


Create a campaign without a goal's guidance
Choose a campaign type first, without a recommendation based on your objective

  • Search Get in front of high-intent customers at the right time on Google Search
  • Performance Max Reach audiences across all of Google with a single campaign. See how it works
  • Display Reach customers across 3 million sites and apps with engaging creative
  • Shopping Showcase your products to shoppers as they explore what to buy
  • Video Reach viewers on YouTube and get conversions
  • App Drive downloads and grow engagement for your app
  • Smart Reach customers with a one-stop solution built for small businesses
  • Demand Gen Drive demand and conversions on YouTube, Discover and Gmail with image and video ads

CampaignType filter



Video Campaign Subtypes:

Video views New Get views and engagement from people who are more likely to consider your products or brand. You only pay when someone chooses to watch your ad. Your ads can show as skippable in-stream, in-feed, and Shorts ads. Learn more

Efficient reach Get the most reach for your budget using bumper, skippable in-stream, in-feed, and Shorts ads. Learn more Reach a broad audience, or reach the same people multiple times. You pay for every 1,000 times your ads are shown (impressions).

Target frequency Reach the same people more than once with skippable in-stream ads, non-skippable in-stream ads, or bumper ads. Learn more

Non-skippable reach Reach people using up to 15-second non-skippable in-stream ads. Learn more

Drive conversions Get more conversions with video ads designed to encourage valuable interactions with your business Learn more

Ad sequence Tell your story by showing ads in a particular sequence to individual viewers with skippable in-stream ads, non-skippable in-stream ads, bumper ads, or a mix. Learn more

Audio Reach your audience while they're listening on YouTube with audio-based ads. Learn more

YouTube subscriptions and engagements New Get subscriptions and drive engagement on your YouTube channel with video ads designed to encourage valuable interactions. Learn more




Interesting docs:

Vanity metrics: DAU, WAU, MAU

DAU, WAU и MAU малочувствительны к изменениям в продукте

dau_total = (
    sessions.groupby('session_date').agg({'client_id': 'nunique'}).mean()
)

wau_total = (
    sessions.groupby(['session_year', 'session_week'])
    .agg({'client_id': 'nunique'})
    .mean()
)

  • Sticky factor - DAU/WAU, DAU/MAU отражает регулярность использования сервиса или приложения 
  • Среднее количество сессий на пользователя за период (обычно берут в расчет месяц)
  • Средняя продолжительность сессии, average session length

sessions['session_duration_sec'] = (
    sessions['session_end_ts'] - sessions['session_start_ts']
).dt.seconds

print(sessions['session_duration_sec'].median()) #mean будет сильно сдвигаться за счет экстремально-длинных сессий
#желательно чтобы медиана была выше некоторого минимального значения (отказ пользователя)

# и распределение
sessions['session_duration_sec'].hist(bins=50)
plt.show() 

пятница, 19 апреля 2024 г.

LTV, CAC, ROI

  • Retention (Churn), Conversion - база для расчета
  • LTV, или Lifetime Value, — это «пожизненная ценность» клиента, то есть общая сумма денег, которую один клиент в среднем приносит компании со всех своих покупок. "В среднем" потому что вычисление происходит для когорты, вся выручка с когорты делится на размер когорты. В теории эта метрика включает все прошлые, нынешние и будущие покупки пользователя. На практике чаще анализируют LTV за определённый срок — первые 1, 3, 7 и 14 дней после регистрации. 
  • CAC, или Customer Acquisition Cost, — стоимость привлечения одного клиента. Сумма денег, в которую компании обходится каждый новый клиент. В сущности, CAC — это инвестиции в маркетинг. 
  • ROI, или Return On Investment, — окупаемость инвестиций. В экономике одного покупателя эта метрика показывает, на сколько процентов LTV превысил CAC. Ещё говорят: на сколько процентов «окупились» клиенты. (=LTV/CAC)
Иногда вместо LTV используют показатели ARPU или ARPPU
  • ARPU, или Average Revenue Per User, — это полный аналог LTV. Рассчитывается делением выручки с накоплением на размер когорты. 
  • ARPPU, или Average Revenue Per Paying User, — тот же LTV, но рассчитанный с учётом только платящих пользователей когорты. Выручку с накоплением делят на число пользователей, совершивших хотя бы одну покупку. За счёт исключения неплатящих пользователей ARPPU часто более показателен, чем ARPU, но считать его труднее: во-первых, число платящих в когорте со временем растёт, а во-вторых, для расчёта требуется больше данных о привлечённых пользователях.
Как проверить вычисления?
  • сумма размеров когорт равна числу новых пользователей за изучаемый период
  • сумма размеров платящих когорт равна числу покупателей за изучаемый период
  • retention rate убывает по экспоненциальному закону e^(-x/a), a - задает кривизну
  • retention rate неплатящих убывает быстрее, чем убывание платащих
  • retention rate не может быть отрицательным
  • conversion rate в когортном анализе никогда не снижается (плавно растущая функция) и не может превышать 100%; 1-e^-(x/a+b), a - кривизна, b - конверсия первого дня
  • количество новых покупателей равно числу новых клиентов умноженному на общую конверсию
  • CAC когорты постоянная величина для всех лайфтаймов
  • CAC умноженный на размер когорты должен быть равен рекламным затратам на изучаемом периоде
  • LTV никогда не снижаются (плавно растущая функция) 
  • LTV теоретически не ограничена сверху
  • Общая стоимость покупок клиентов когорты равна количеству этих клиентов умноженному на максимальных LTV
  • Если LTV и CAC рассчитаны верно это исключает ошибки в исходных данных и ROI скорее всего тоже рассчитан верно
  • Реалистичный ROI лежит в диапазоне от 0 до 3 (если ROI больше 300% это либо чудо либо ошибка)















суббота, 6 апреля 2024 г.

пятница, 5 апреля 2024 г.

Пару слов о CRM

Customer relationship management
(хаос нельзя автоматизировать можно автоматизировать порядок)

Система управления взаимоотношениями с клиентами, построена вокруг клиентов. 
  • предоставляет полный обзор взаимоотношений с клиентом;
  • оперирует понятием воронка (лид - квалифицированный лид - продажа - повторная продажа);
  • позволяет персонализировать коммуникацию с клиентом;
  • управление ожиданиями клиента (с вами свяжутся через хх часов и др. транзакционные рутинные сообщения, оценка удовлетворенности клиента);
  • прогревать лиды полученные на вторичных конверсиях, например с помощью лидмагнитов;
  • аналитика по воронке, по когортам, по менеджерам и по всему процессу;
  • позволяет установить KPI;
  • позволяет узнать количество сделок в работе (и на каждом этапе);
  • позволяет оценить загрузку менеджеров;
  • для запуска необходимы хорошо прописанные бизнес процессы (кто и что делает на каждом этапе, какую информацию получить, какую информацию передать на следующий этап;
  • похоже на Jira, где клиент это epic, а коммуникация с ним это таски 🙂;
  • CRM позволяет объединять множество лэндингов для привлечения лидов и др. каналов, все обращения должны падать в одно место;
  • измерение всех этапов воронки помогает выявлять узкие места и управлять. Снижать стоимость Лида, увеличилось количество первичных покупок, увеличивать количество повторных обращений и т.п.;
  • влияние того или иного фактора на результат, например "количество встреч" может не влиять. Очевидно что поле для анализируемого фактора должно поддерживаться в CRM;
  • взаимозаменяемость и масштабируемость продаж, увеличение скорости горизонтального взаимодействия между отделами;
  • в CRM полезно фиксировать: страницу первого входа для анализа контента, источник первого входа, наличие регистрации на вебинар, лидмагнитов, страница оставления заявки, источник оставления заявки, это может быть домен в случае использования множества лэндингов;
  • стимулирование повторных продаж, составление customer journey map и коммуникация с клиентом (по триггерам, условиям и сегментам);

Полезное:
  • поля сегментации оформить списком и сделать обязательными;
  • контент для различных каналов должен быть разным;
  • отвечать на письма особенно если лидов мало;
  • обязательно брать NPS Net Promoter Score особенно первый раз (опросный лист что понравилось, а что нет);
  • работать с отказниками или зависшими пользователями, спрашивать прямой вопрос что случилось?;
  • если лидов мало снимать NPS личными сообщениями, это позволит выявить скрытые детали;