Собесов

InterviewQuery Pandas — конверсия воронки и drop-off

Pythonpandas — воронкиСредняяMiddle

Условие

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

Идея

  1. Сводим к pivot: индекс юзер, колонки — шаги, значения — первое время шага.
  2. passed[step] = passed[prev] AND time[step] > time[prev] — строгий порядок.
  3. Суммируем и считаем drop_off / conversion.

Тонкость с pivot и одинаковыми временами

Если у юзера view и click в один момент — passed[click] = False (так как строгое >). Это правильно для строгой воронки.

Подводные камни

  1. Юзеры без view. Их нет в pivoted — норм, в passed[view]False.
  2. .shift(1) на первой строке. Даст NaN — для top-step conv_from_prev = NaN, что логично.
  3. drop_off отрицательный. Невозможно по семантике (passed монотонно убывает), но если в воронке нарушен порядок шагов — ошибка вылезет наружу.
  4. Pivot без aggfunc. Если у юзера два события одного типа — pivot упадёт. У нас уже groupby.min() это решает.

Эталонный ответ

Pivot по user×event_type с MIN(time), затем cumulative AND-проверка строгого нарастания, агрегация sum по флагам. Стандартный pandas-паттерн воронки.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти