Собесов

InterviewQuery Pandas — RFM-сегментация через qcut

Pythonpandas — сегментацияСредняяMiddle

Условие

DataFrame orders(user_id, order_date, amount). Постройте RFM-сегментацию: 5 квинтилей по recency / frequency / monetary, итоговый сегмент R-F-M.

Решение

import pandas as pd
 
def rfm(orders: pd.DataFrame, today=None) -> pd.DataFrame:
    today = pd.Timestamp(today) if today else pd.Timestamp.today().normalize()
    df = orders.copy()
    df["order_date"] = pd.to_datetime(df["order_date"])
 
    rfm = (
        df.groupby("user_id", as_index=False)
          .agg(
              recency_days=("order_date", lambda s: (today - s.max()).days),
              frequency=("user_id", "size"),
              monetary=("amount", "sum"),
          )
    )
 
    # qcut: 1 = низ, 5 = верх. Инвертируем recency
    rfm["R"] = pd.qcut(rfm["recency_days"], 5, labels=[5, 4, 3, 2, 1]).astype(int)
    rfm["F"] = pd.qcut(rfm["frequency"].rank(method="first"), 5,
                       labels=[1, 2, 3, 4, 5]).astype(int)
    rfm["M"] = pd.qcut(rfm["monetary"],  5, labels=[1, 2, 3, 4, 5]).astype(int)
 
    rfm["segment"] = rfm["R"].astype(str) + "-" + rfm["F"].astype(str) + "-" + rfm["M"].astype(str)
    return rfm

Зачем rank(method='first') для frequency

pd.qcut падает с ValueError: Bin edges must be unique, если в данных слишком много одинаковых значений (например, 60% юзеров имеют ровно 1 заказ). rank(method='first') ломает ties монотонно — qcut получает уникальные значения.

Альтернатива — pd.qcut(..., duplicates='drop'), но тогда квинтилей будет меньше 5.

Инверсия для recency

Recency: меньше = лучше. Поэтому labels=[5, 4, 3, 2, 1] — низ recency_days получает метку 5.

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

  1. qcut vs cut. qcut режет по квантилям (равное число точек в группе), cut — по равным интервалам значений. Для RFM — почти всегда qcut.
  2. labels тип. Без .astype(int) метки — categorical, не int. Конкатенация работает, но арифметика — нет.
  3. today параметр. Жёсткое pd.Timestamp.today() ломает тесты. Лучше параметр.
  4. NaN после groupby + agg. Если у user_id нет заказов — он не в rfm. Для inactive users нужно LEFT JOIN с user dimension.
  5. Time zone. Если order_date со tz, а today без — (today - s.max()).days падает с ошибкой. Привести к одному tz.

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

groupby.agg(recency, frequency, monetary), потом pd.qcut(.., 5, labels=...) с инверсией для recency. Для frequency — обязательно .rank(method='first') ради duplicates.

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

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

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