Собесов

Aviasales Booking — Pandas: pivot по сопутствующим услугам

PythonPandas и аналитикаСредняяMiddle

Условие

Дан CSV booking_analytics_test_df.csv с заказами и сопутствующими услугами Aviasales (страховки, изменения, уведомления и т. д.). Поля:

  • order_id — уникальный идентификатор заказа,
  • order_price — цена заказа,
  • service — название услуги,
  • service_price — цена услуги,
  • service_profit — профит от услуги,
  • origin_country, destination_country — страны вылета и прилёта,
  • order_platform — платформа (desktop, ios, android),
  • booking_depth — глубина бронирования (дней между покупкой и поездкой),
  • is_return0 в одну сторону, 1 туда-обратно,
  • adults, children — пассажиры в заказе,
  • order_date — дата покупки.

Задание:

  1. С помощью pandas соберите таблицу: в строках — наименование услуги, в столбцах — минимальная и максимальная цена услуги, минимальный и максимальный профит от услуги, количество заказов с услугой.
  2. Соберите таблицу: в строках — услуга, в столбцах — цена и профит услуг по топ-5 странам вылета по продажам билетов.

Структура данных

CSV ~10–20k строк. Пример:

order_id,order_price,service,service_price,service_profit,origin_country,...,order_date
KPD24O68HE2O,6090.0,changes,499.00,399.20,RU,RU,desktop,15.43,0,1.0,0,2024-01-05
KRR4DN4PG7OG,18280.0,notification,299.00,271.77,AM,TR,desktop,32.42,0,2.0,0,2024-01-17

Один order_id обычно с одной услугой; но возможно повторение — стоит проверить.

Решение

Подход

Чистый Pandas groupby + agg + pivot_table. Главная тонкость — «количество заказов» vs «строк», и фильтрация на топ-5 стран.

Реализация

import pandas as pd
 
df = pd.read_csv("booking_analytics_test_df.csv", parse_dates=["order_date"])
print(df.shape)
print(df.head())
 
# Sanity: есть ли повторяющиеся order_id?
print("order_id unique:", df["order_id"].nunique(), "rows:", len(df))

Вопрос 1. Сводная по услуге

q1 = df.groupby("service").agg(
    min_service_price=("service_price", "min"),
    max_service_price=("service_price", "max"),
    min_service_profit=("service_profit", "min"),
    max_service_profit=("service_profit", "max"),
    orders_count=("order_id", "nunique"),
).round(2)
 
print(q1.sort_values("orders_count", ascending=False))

nunique — на случай дубликатов order_id (один заказ + одна услуга может прилететь дважды из ETL).

Вопрос 2. Цена и профит по топ-5 origin_country

# Шаг 1. Топ-5 стран по числу заказов
top5_origin = (
    df.groupby("origin_country")["order_id"]
      .nunique()
      .sort_values(ascending=False)
      .head(5)
      .index
      .tolist()
)
print("Top-5 origin:", top5_origin)
 
# Шаг 2. Фильтр + pivot
df_top = df[df["origin_country"].isin(top5_origin)]
 
q2 = df_top.pivot_table(
    index="service",
    columns="origin_country",
    values=["service_price", "service_profit"],
    aggfunc="mean",
).round(2)
 
print(q2)

Для compact-вывода стоит упорядочить столбцы:

q2 = q2.reindex(columns=top5_origin, level="origin_country")

Проверка

# Контрольная сумма: сумма orders_count в q1 ≈ числу строк (если по 1 услуге на заказ)
assert q1["orders_count"].sum() <= df["order_id"].nunique() * 5  # услуг немного
 
# В q2 не должно быть стран вне top5
assert set(q2.columns.get_level_values("origin_country")) == set(top5_origin)

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

  1. count vs nunique. Если order_id дублируется (несколько строк на один заказ), count посчитает строки, а нам нужны уникальные заказы. Использовать nunique.
  2. «Топ-5 стран по продажам билетов» — что это? Чаще всего «по числу заказов». Но может быть «по выручке» (order_price.sum()) — уточнить. У нас взяли по числу заказов; если интервью требует выручку — заменить.
  3. Услуга с одной строкой — даст min == max == price. Если в задании важна вариативность — добавить колонку n_observations.
  4. pivot_table(aggfunc="mean") или "sum"? В вопросе 2 говорится «цена и профит услуг» — двусмысленно. Я взял mean (логично, цена услуги — характеристика); если нужно «общая выручка» — sum.
  5. origin_country = NA. Если в данных есть пропуски — они попадут в groupby как отдельная категория; dropna=False или явный фильтр.
  6. service с пропусками. Заказы без услуг могут идти как service = NaN. Удалите или вынесите в отдельную строку.
  7. Округление. Цены и профит — обычно 2 знака. Не выводите 123.456789012 — это «грязный» отчёт.

Альтернативы

Можно сделать через crosstab или pivot_table с margins=True для добавления «Итого» по столбцам/строкам — для слайда руководству удобно.

df.pivot_table(
    index="service", columns="origin_country",
    values="service_price", aggfunc="mean", margins=True, margins_name="Total"
)

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

# Q1
q1 = df.groupby("service").agg(
    min_service_price=("service_price", "min"),
    max_service_price=("service_price", "max"),
    min_service_profit=("service_profit", "min"),
    max_service_profit=("service_profit", "max"),
    orders_count=("order_id", "nunique"),
)
 
# Q2
top5 = df.groupby("origin_country")["order_id"].nunique().nlargest(5).index
q2 = (df[df["origin_country"].isin(top5)]
        .pivot_table(index="service", columns="origin_country",
                     values=["service_price", "service_profit"], aggfunc="mean"))

Главные акценты — nunique для заказов и фильтрация на топ-5 до pivot.

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

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

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