Условие
DataFrame events(user_id, event_type, event_time). Шаги воронки: ['view', 'click', 'add_to_cart', 'purchase']. Постройте DataFrame:
| step | users | drop_off | conv_from_top | conv_from_prev |
Учитывайте строгую последовательность по времени (как в SQL-кейсе).
Решение
import pandas as pd
FUNNEL = ["view", "click", "add_to_cart", "purchase"]
def funnel(events: pd.DataFrame, steps=FUNNEL) -> pd.DataFrame:
df = events.copy()
df["event_time"] = pd.to_datetime(df["event_time"])
# Минимальное время каждого шага для каждого юзера
pivoted = (
df[df["event_type"].isin(steps)]
.groupby(["user_id", "event_type"], as_index=False)["event_time"].min()
.pivot(index="user_id", columns="event_type", values="event_time")
)
# Проверка строгого нарастания
passed = pd.DataFrame(index=pivoted.index)
passed[steps[0]] = pivoted[steps[0]].notna()
for i in range(1, len(steps)):
prev, curr = steps[i - 1], steps[i]
passed[curr] = (
passed[prev]
& pivoted[curr].notna()
& (pivoted[curr] > pivoted[prev])
)
counts = passed.sum().reindex(steps).rename("users").to_frame()
counts["drop_off"] = counts["users"].shift(1) - counts["users"]
counts["conv_from_top"] = counts["users"] / counts["users"].iloc[0]
counts["conv_from_prev"] = counts["users"] / counts["users"].shift(1)
counts = counts.round(4).reset_index().rename(columns={"index": "step"})
return countsИдея
- Сводим к pivot: индекс юзер, колонки — шаги, значения — первое время шага.
passed[step] = passed[prev] AND time[step] > time[prev]— строгий порядок.- Суммируем и считаем drop_off / conversion.
Тонкость с pivot и одинаковыми временами
Если у юзера view и click в один момент — passed[click] = False (так как строгое >). Это правильно для строгой воронки.
Подводные камни
- Юзеры без
view. Их нет в pivoted — норм, вpassed[view]—False. .shift(1)на первой строке. ДастNaN— для top-stepconv_from_prev = NaN, что логично.drop_offотрицательный. Невозможно по семантике (passed монотонно убывает), но если в воронке нарушен порядок шагов — ошибка вылезет наружу.- Pivot без
aggfunc. Если у юзера два события одного типа —pivotупадёт. У нас ужеgroupby.min()это решает.
Эталонный ответ
Pivot по user×event_type с MIN(time), затем cumulative AND-проверка строгого нарастания, агрегация sum по флагам. Стандартный pandas-паттерн воронки.