Условие
В логе events(user_id, ts, event) есть события view, add_to_cart, checkout, purchase. Построить воронку: сколько уникальных пользователей дошли до каждого шага, при условии что шаги шли по порядку.
Решение
Подход (упорядоченная воронка)
Для каждого пользователя — найти время первого view, затем первого add_to_cart после view, и так далее. Считать пользователей с непустым каждым шагом.
import pandas as pd
steps = ['view', 'add_to_cart', 'checkout', 'purchase']
# Время первого каждого события у каждого юзера
first = (
events[events['event'].isin(steps)]
.groupby(['user_id', 'event'])['ts'].min()
.unstack('event')
)
# Применяем порядок: следующий шаг должен быть после предыдущего
mask = pd.Series(True, index=first.index)
funnel = {}
for prev, curr in zip([None] + steps[:-1], steps):
if prev is None:
mask &= first[curr].notna()
else:
mask &= first[curr].notna() & (first[curr] >= first[prev])
funnel[curr] = mask.sum()
print(funnel)Конверсии шаг-к-шагу
fdf = pd.Series(funnel)
fdf_pct = (fdf / fdf.iloc[0] * 100).round(1)
step_to_step = (fdf / fdf.shift(1) * 100).round(1)Воронка с окном (внутри сессии / 24 часов)
Если шаги должны быть в пределах окна, фильтруем first[curr] - first[prev] <= pd.Timedelta('24h').
Подводные камни
- Без условия порядка воронка превращается в просто число уникальных пользователей по событию — это не воронка.
- Дубли событий (несколько
viewот одного юзера) —groupby ... minрешает. - Если временной шаг важен в сессии, надо сначала сессионизировать, потом строить воронку внутри сессии.
- NaN в
first[step]≠ 0 —maskдолжна явно требовать.notna(). - Алгоритм линейный по сегментам, но для A/B-теста воронки нужно строить отдельно по группам —
groupby('variant').
Эталонный ответ
Для каждого пользователя — время первого события каждого шага через groupby.unstack. Маска: каждый следующий ts ≥ предыдущего. mask.sum() по шагам = воронка.