CAC в pandas: как посчитать стоимость нового покупателя по каналам

Реклама привела сто заказов, но восемьдесят из них сделали прежние покупатели. Деление бюджета на число заказов даёт стоимость заказа, а руководитель спрашивал стоимость привлечения нового клиента. Построим CAC по первой оплаченной покупке и явно выберем канал привлечения.

Новый клиент определяется по полной истории

Первую покупку ищут до фильтрации отчётного месяца. Если сначала оставить октябрь, сентябрьский покупатель с октябрьским заказом станет новым. В примере история начинается до анализируемого периода; в реальной базе потребуется проверить дату начала наблюдения и миграции CRM.

Канал первой покупки принимаем за канал приобретения. Это простое правило атрибуции. Оно отвечает на вопрос, где записана первая продажа, и не распределяет заслугу между касаниями. Менять правило между месяцами нельзя без пересчёта истории.

Разделяем поиск первого заказа и расчёт месяца

Сортировка включает order_id, чтобы одинаковое время не делало результат случайным. После drop_duplicates остаётся один первый заказ на клиента. Затем применяем границы месяца и считаем покупателей по каналу. Бюджет присоединяем к этой таблице, сохраняя каналы без приобретений.

import pandas as pd
o = pd.DataFrame({"order_id":[1,2,3,4,5],"customer":["old","old","new1","new1","new2"],"date":["2026-09-20","2026-10-02","2026-10-03","2026-10-10","2026-10-04"],"channel":["search","search","social","social","search"]})
o["date"] = pd.to_datetime(o.date)
first = o.sort_values(["date","order_id"]).drop_duplicates("customer")
new = first[first.date.ge("2026-10-01") & first.date.lt("2026-11-01")]
counts = new.groupby("channel").customer.nunique().rename("new_customers")
spend = pd.Series({"search":600,"social":400,"display":100},name="spend")
r = spend.to_frame().join(counts)
r["new_customers"] = r.new_customers.fillna(0).astype(int)
r["CAC"] = r.spend.div(r.new_customers.where(r.new_customers.gt(0)))
print(r.to_string())
assert r.loc["search","new_customers"] == 1
assert r.loc["social","CAC"] == 400
assert pd.isna(r.loc["display","CAC"])

В search один новый покупатель и CAC 600; старый клиент не увеличивает знаменатель. В social CAC равен 400, хотя новый покупатель сделал два заказа. Для display стоимость привлечения не определена: расход есть, приобретений нет. В отчёте это повод разобрать кампанию, а не записывать нулевой CAC.

Показатель нужен для сопоставления с будущей маржой приобретённой когорты. Дешёвый канал может приводить покупателей с маленьким чеком и высоким возвратом, дорогой — более прибыльных. Поэтому рядом сохраняют число новых клиентов, размер бюджета и правило атрибуции. Без этих полей один CAC легко превращается в рейтинг каналов, который нельзя объяснить.

Как использовать CAC для следующего бюджета

Разница 600 против 400 сама по себе не означает, что весь бюджет стоит перенести в social. Каналы могут приобретать разных клиентов. Для решения полезно построить когорты первых покупателей и посмотреть маржу за одинаковый срок после привлечения. Если покупатель search возвращается несколько раз, более высокий CAC может окупаться. Если social даёт много возвратов, низкая цена первой покупки выглядит лучше реального результата.

Особое внимание требуется к клиентскому идентификатору. Один человек может покупать с телефона и компьютера, менять email или оформлять заказ без регистрации. Если в данных он получает два customer_id, отчёт дважды посчитает приобретение. Обратная ошибка возникает, когда вся семья использует одну карту или общий аккаунт. В аналитической записке фиксируют именно доступную единицу: новый CRM-клиент, новый аккаунт или новый подтверждённый покупатель.

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

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

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

Стоимость приобретения стоит сопоставлять с доходом той же когорты на одинаковом сроке: LTV когорты в SQL: одинаковое окно наблюдения.

Проверяем приобретения по клиентскому списку

В примере удобно вручную открыть first: old должен иметь сентябрьскую дату, new1 — 3 октября, new2 — 4 октября. Затем сумма новых покупателей по каналам должна совпасть с числом строк new. Это простой контроль полноты распределения: если часть клиентов потеряла канал при объединении, общий CAC изменится, даже когда формула деления остаётся правильной.

Следующая рабочая задача

В «Практической аналитике данных» вы разбираете рекламные каналы вместе с продажами и клиентским удержанием. Практика учит выбрать метрику под вопрос, проверить исходные данные и превратить расчёт в рекомендацию с понятными границами. Откройте программу курса «Практическая аналитика данных».

Продолжить на практике

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

    Превратите знания SQL или Python в аналитическую работу: проверяйте данные, считайте бизнес-метрики, стройте дашборды и объясняйте выводы на трёх прикладных кейсах.