Урок курса

Взвешенное среднее и эффект состава групп

Практическая аналитика данных

В прошлом уроке мы читали форму распределения через частотную таблицу и гистограмму. Теперь нас интересует один конкретный агрегат — средний чек по значениям gross_revenue — и то, как он ведёт себя при разбивке заказов на группы.

Средний чек как отношение агрегатов и формула взвешенного среднего

Средний чек (AOV, Average Order Value) — это не среднее по строкам таблицы позиций, а отношение двух агрегатов по заказам:

AOV = суммарная gross_revenue paid-заказов / число paid-заказов

Grain здесь — заказ: каждый paid order_id входит в знаменатель ровно один раз и вносит свою gross_revenue в числитель. Строки order_items в этот расчёт не попадают вовсе. Например, три заказа на 600, 900 и 1 500 руб. дадут AOV = (600 + 900 + 1 500) / 3 = 1 000 руб.

Когда все заказы рассматриваются как равнозначные наблюдения, это обычное арифметическое среднее. Но как только мы делим заказы на группы и хотим получить итоговый AOV из групповых средних, не суммируя заново все исходные значения, нужна другая формула.

Взвешенное среднее назначает каждому значению xᵢ вес wᵢ — число единиц, которое это значение представляет:

x̄_w = Σ(xᵢ · wᵢ) / Σwᵢ

Простое арифметическое среднее — частный случай этой формулы при всех wᵢ = 1: тогда Σwᵢ = N и x̄_w = Σxᵢ / N.

Расхождение между простым и взвешенным средним возникает, когда одновременно выполняются два условия: средние групп различаются и размеры групп неравны. Если средние групп одинаковы, простое и взвешенное среднее совпадут при любом соотношении размеров.

Для двух групп с различными средними направление расхождения определяется однозначно: простое среднее переоценивает вклад меньшей группы и недооценивает вклад большей. Поэтому оно завышает итоговый AOV, если меньшая группа дороже, и занижает, если дороже большая.

При трёх и более группах неравные размеры и различающиеся средние — необходимые, но не достаточные условия расхождения. Вся совокупность средних и весов определяет итог: при определённых их сочетаниях разнонаправленные отклонения могут компенсироваться, и простое среднее совпадёт с взвешенным даже при неравных размерах групп.

Ценовые сегменты, веса и тождество взвешенного AOV

Ценовой сегмент — это способ разбить paid-заказы на непересекающиеся группы по диапазону gross_revenue. Например:

  • «эконом»: gross_revenue < 1 000 руб.
  • «средний»: gross_revenue ≥ 1 000 руб.

Каждый заказ попадает ровно в один сегмент. Внутри сегмента i вычисляется собственный средний чек:

AOV_i = sum_i / n_i

где sum_i — суммарная gross_revenue заказов сегмента, n_i — их число. Вес сегмента при взвешенном среднем — это n_i. Веса в сумме дают общее число paid-заказов: Σn_i = total_n.

Тождество. Если сегменты покрывают все заказы без перекрытий, взвешенное среднее AOV по сегментам тождественно совпадает с прямым общим AOV:

Σ(AOV_i · n_i) / Σn_i = Σ(sum_i / n_i · n_i) / Σn_i = Σsum_i / Σn_i = total_sum / total_n

Раскрытие показывает: n_i в числителе и знаменателе AOV_i сокращаются, и остаётся просто сумма всех gross_revenue, делённая на общее число заказов.

Числовой пример (учебный). Два сегмента paid-заказов:

  • Эконом: 40 заказов, суммарная gross_revenue = 24 000 руб. → AOV_эконом = 600 руб.
  • Средний: 10 заказов, суммарная gross_revenue = 20 000 руб. → AOV_средний = 2 000 руб.

Простое среднее средних: (600 + 2 000) / 2 = 1 300 руб.

Взвешенное среднее: x̄_w = (600 · 40 + 2 000 · 10) / (40 + 10) = (24 000 + 20 000) / 50 = 44 000 / 50 = 880 руб.

Прямой общий AOV: (24 000 + 20 000) / (40 + 10) = 44 000 / 50 = 880 руб. ✓

Простое среднее завышает результат на 420 руб. — потому что неявно считает, что заказов «эконом» и «среднего» поровну, хотя их соотношение 4:1.

Почему совпадения AOV недостаточно для проверки. Тождество работает только при полном покрытии без перекрытий. Пропущенные или задвоенные заказы могут случайно сохранить то же числовое значение AOV, но счёт уже будет неверным. Поэтому покрытие проверяют отдельно: Σn_i должна совпасть с независимо посчитанным COUNT paid-заказов, а Σsum_i — с независимым SUM gross_revenue.

Вычисление взвешенного AOV по сегментам: SQL и pandas

SQLite-маршрут

Запрос строится в два слоя. CTE segments группирует paid-заказы по сегменту и считает агрегаты; основной SELECT вычисляет AOV каждого сегмента и взвешенное среднее через оконные функции:

WITH segments AS (
  SELECT
    CASE
      WHEN gross_revenue < 1000 THEN 'эконом'
      ELSE 'средний'
    END AS segment,
    COUNT(order_id)    AS n,
    SUM(gross_revenue) AS total_rev
  FROM orders
  WHERE status = 'paid'
    AND gross_revenue IS NOT NULL
  GROUP BY segment
)
SELECT
  segment,
  n,
  total_rev,
  CAST(total_rev AS REAL) / n              AS aov_segment,
  SUM(CAST(total_rev AS REAL)) OVER ()     AS grand_total_rev,
  SUM(n) OVER ()                           AS grand_n,
  SUM(CAST(total_rev AS REAL)) OVER () /
    SUM(n) OVER ()                         AS weighted_aov
FROM segments;

CASE WHEN гарантирует, что каждый заказ получает ровно одну метку сегмента. CAST(... AS REAL) нужен потому, что SQLite делит целые числа целочисленно — без него total_rev / n потеряет дробную часть. Оконные SUM(...) OVER () считаются по всем строкам результата и дают grand_total_rev и grand_n без дополнительного подзапроса.

Независимая проверка покрытия — отдельный запрос к исходной таблице:

SELECT
  COUNT(*)                                         AS direct_n,
  SUM(gross_revenue)                               AS direct_sum,
  1.0 * SUM(gross_revenue) / NULLIF(COUNT(*), 0)  AS direct_aov
FROM orders
WHERE status = 'paid'
  AND gross_revenue IS NOT NULL;

Сравниваем: grand_n с direct_n, grand_total_rev с direct_sum, weighted_aov с direct_aov. Повторное деление grand_total_rev / grand_n не считается независимой проверкой — это те же числа из CTE.


pandas-маршрут

import pandas as pd

df = pd.read_csv('orders.csv')
paid = df.loc[df['status'].eq('paid') & df['gross_revenue'].notna()].copy()

# Назначить сегмент каждому заказу
paid['segment'] = pd.cut(
    paid['gross_revenue'],
    bins=[0, 1000, float('inf')],
    labels=['эконом', 'средний'],
    right=False
)

# Проверить отсутствие неназначенных сегментов — до groupby
assert paid['segment'].notna().all(), "Есть заказы с gross_revenue < 0 — они не попали ни в один сегмент"

# Агрегаты по сегменту
seg = paid.groupby('segment', observed=True).agg(
    n=('order_id', 'count'),
    total_rev=('gross_revenue', 'sum')
).reset_index()
seg['aov_segment'] = seg['total_rev'] / seg['n']

# Взвешенное среднее
weighted_aov = (seg['aov_segment'] * seg['n']).sum() / seg['n'].sum()

# Независимая проверка покрытия
direct_aov = paid['gross_revenue'].sum() / len(paid)
assert seg['n'].sum() == len(paid)                           # число заказов
assert seg['total_rev'].sum() == paid['gross_revenue'].sum() # суммарная выручка
print(weighted_aov, direct_aov)  # должны совпасть

pd.cut с right=False задаёт полуоткрытые интервалы [0, 1000) и [1000, ∞). Граничное значение 1 000 руб. попадает в «средний», а не в «эконом». Значение 0 руб. входит в «эконом» — нижняя граница включена. NaN возникает только для значений строго меньше 0, выходящих за левую границу первого интервала. Именно поэтому проверку paid['segment'].notna().all() нужно делать сразу после pd.cut, до groupby: по умолчанию groupby исключает строки с ключом NaN (параметр dropna=True), и пост-агрегационная проверка просто не увидит потерянные заказы. observed=True оставляет в результате только реально наблюдаемые категории, подавляя предупреждение для Categorical-столбца.

Если данные расширятся и появятся заказы с отрицательной gross_revenue, pd.cut выдаст NaN и assert упадёт — это сигнал перепроверить допустимость значений до сегментации.

В обоих маршрутах итоговое значение weighted_aov совпадёт с direct_aov — это подтверждает тождество из предыдущего раздела на реальных данных файла.

Эффект состава: сдвиг долей сегментов меняет итоговый AOV без изменения внутренних значений

Итоговый AOV зависит не только от того, сколько платят внутри каждого сегмента, но и от того, какой сегмент преобладает. Если доля дорогих заказов выросла, а AOV каждого сегмента в отдельности остался прежним, итоговый AOV всё равно поднимется. Это называют mix-shift — сдвигом состава.

Учебный числовой пример. AOV внутри сегментов зафиксирован в обоих периодах:

  • AOV_эконом = 600 руб.
  • AOV_средний = 2 000 руб.

Период A: 40 заказов «эконом», 10 заказов «средний» (итого 50).

AOV_A = (600 · 40 + 2 000 · 10) / 50 = 44 000 / 50 = 880 руб.

Период B: 10 заказов «эконом», 40 заказов «средний» (итого 50). Внутренние AOV не изменились.

AOV_B = (600 · 10 + 2 000 · 40) / 50 = 86 000 / 50 = 1 720 руб.

Рост итогового AOV: +840 руб., или почти вдвое. Ни один сегмент не подорожал. Причина — доля «среднего» сегмента выросла с 20 % до 80 %.

Это разделяет два принципиально разных источника изменения агрегата:

  • Рост значений внутри групп — заказы стали дороже сами по себе: AOV_i изменился.
  • Сдвиг весов групп — состав продаж сместился в сторону другого сегмента: AOV_i прежние, но n_i другие.

Диагностика straightforward: зафиксировать AOV_i в обоих периодах и сравнить веса n_i. Если AOV_i не изменились, а итоговый AOV изменился — причина в составе, а не в ценах.

Важная оговорка: неизменный итоговый AOV не означает неизменного состава. Разнонаправленные сдвиги нескольких сегментов могут компенсировать друг друга, сохраняя итог на том же уровне при реально изменившейся структуре продаж. Поэтому смотреть только на итоговое число недостаточно — нужно раскладывать его по сегментам.

Задача 1

Отдельный условный пример. Магазин «Маяк» за месяц выполнил paid-заказы в двух ценовых сегментах (данные уже агрегированы):
— Эконом (gross_revenue < 1000 руб.): 60 заказов, суммарная gross_revenue = 36 000 руб.
— Средний (gross_revenue ≥ 1000 руб.): 15 заказов, суммарная gross_revenue = 30 000 руб.

Вычислите взвешенный AOV по сегментам, используя в качестве весов число заказов в каждом сегменте. Введите ответ целым числом (в рублях).

Без регистрации · результат не сохраняется

Задача 2

Отдельный условный пример. Аналитик магазина «Маяк» вычислил AOV по двум ценовым сегментам paid-заказов:
— Эконом: AOV = 500 руб. (80 заказов)
— Средний: AOV = 2500 руб. (20 заказов)

Он сообщил руководителю: «Общий средний чек = (500 + 2500) / 2 = 1500 руб.» Прямой расчёт даёт (80·500 + 20·2500) / 100 = 900 руб. Почему результаты расходятся?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Отдельный условный магазин. Для двух непересекающихся сегментов известны агрегаты:

Сегмент | paid-заказы | товарные позиции в этих заказах | AOV одного paid-заказа
Эконом | 36 | 72 | 500 руб.
Средний | 24 | 96 | 1750 руб.

Все суммы gross известны, возвраты здесь не учитываются. AOV магазина требуется на уровне заказа, а не товарной позиции. Самостоятельно выберите подходящие веса и составьте расчёт; готовая формула не задана. Заполните семь результатов:
(1) вес сегмента «эконом»: ___
(2) вес сегмента «средний»: ___
(3) общее число наблюдений целевой метрики: ___
(4) сумма gross_revenue сегмента «эконом», руб.: ___
(5) сумма gross_revenue сегмента «средний», руб.: ___
(6) общий взвешенный AOV магазина, руб.: ___
(7) общая сумма gross_revenue магазина, руб.: ___

Введите семь значений (1)–(7) по порядку через ; без разделителей тысяч. Допустимы запятые и пробелы после разделителей.

Без регистрации · результат не сохраняется

Задача 4

Отдельный условный пример. В периоде A магазин «Маяк» имел 70 заказов «эконом» (AOV = 600 руб.) и 30 заказов «средний» (AOV = 2000 руб.). В периоде B — 30 заказов «эконом» (AOV = 600 руб.) и 70 заказов «средний» (AOV = 2000 руб.). AOV внутри каждого сегмента не изменился.

Ответьте на два вопроса (2–3 предложения каждый):

  1. Вычислите взвешенный AOV для периода A и для периода B. На сколько рублей он изменился?
  2. Как называется эффект, который вы наблюдаете, и что именно его вызвало — рост цен в сегментах или что-то другое?

Сначала самостоятельно запишите объяснение. Затем выберите объяснение роста общего AOV. Платформа проверяет выбранный вариант; собственный текст сверяете с теорией самостоятельно.

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 5

pandas-маршрут. В Jupyter-ноутбуке выполните следующее:

  1. Загрузите orders.csv через pd.read_csv.
  2. Отберите paid-заказы и оставьте только непустые значения gross_revenue.
  3. Разбейте заказы на два ценовых сегмента с помощью pd.cut:
    — 'эконом': [0, 1000)
    — 'средний': [1000, ∞)
  4. Агрегируйте по сегменту: получите число заказов (n) и суммарную gross_revenue (total_rev); вычислите AOV каждого сегмента.
  5. Вычислите взвешенный AOV по сегментам: Σ(aov_segment · n) / Σn.
  6. Вычислите прямой общий AOV: paid['gross_revenue'].sum() / len(paid).
  7. Выведите таблицу сегментов (segment, n, total_rev, aov_segment) и оба итоговых значения AOV. Сверьте также сумму n с числом выбранных непустых paid-заказов, сумму total_rev с их исходной суммой и отсутствие пропущенного segment. Одно совпадение AOV не доказывает покрытие.

Доступный API: pd.read_csv, DataFrame.copy, DataFrame[col], pd.cut, groupby, agg, reset_index, Series.sum, len.

Можно выполнить те же вычисления и проверки на SQL в mayak.sqlite: CASE для сегмента, GROUP BY, COUNT и SUM; выполните один маршрут. После отработки на всей выбранной совокупности повторите вычисления для полного июня 2024 (order_date >= 2024-06-01 и < 2024-07-01, status=paid, известная gross_revenue до возвратов). Сохраните таблицу segment,n,total_rev,aov_segment и сверки числа строк, суммы и отсутствия пропущенного segment. Отправьте два числа: взвешенный gross-AOV июня (до двух десятичных знаков); число июньских заказов в сегменте «эконом». Формат: aov;count. Допустимы точка или запятая в десятичном числе и пробел после ;. Код и таблицу проверяете самостоятельно; платформа проверяет эти два результата.

Без регистрации · результат не сохраняется

Задача 6

Сопоставьте каждое утверждение о расчёте AOV с его статусом: «верно» или «неверно», и с кратким обоснованием.

Утверждения:
(1) Одного совпадения взвешенного AOV с исходным общим AOV достаточно, чтобы доказать отсутствие пропущенных и повторённых заказов.
(2) Простое среднее AOV сегментов всегда равно взвешенному среднему AOV.
(3) При сдвиге состава заказов в сторону более дорогого сегмента взвешенный AOV вырастет, даже если AOV внутри каждого сегмента не изменился.
(4) Вес сегмента при расчёте взвешенного AOV заказов — это число товарных позиций в сегменте.

Обоснования:
А. Неверно: одинаковое отношение возможно случайно; каждый исходный ключ и независимые суммы/числа проверяют отдельно.
Б. Неверно: равные размеры достаточны, но не необходимы; при одинаковых средних групп результат совпадёт и при разных весах.
В. Верно: перенос доли из более дешёвого сегмента в более дорогой при фиксированных разных средних повышает итог. Произвольные изменения весов нескольких групп могут компенсироваться.
Г. Неверно: вес — это число заказов (order_id), а не позиций; grain расчёта AOV — заказ.

Введите соответствия по порядку номеров, например 1X;2Y;3Z (буквы в примере условные). Допустимы точки с запятой или запятые, с пробелами или без них.

Без регистрации · результат не сохраняется

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку