Уровень «Хард».
Часто нам нужно распределить бюджет какой-то акции/программы так, чтобы…Это «чтобы» может отличаться от задачи к задаче, но неизменным остаётся знание, что чем больше денег мы потратим, тем более выраженные результаты мы получим.
В этой статье мы рассмотрим возможные варианты распределения бюджета на конкретном кейсе: категорийном кэшбэке.
Постановка задачи
В общем случае, нам часто нужно распределить некоторую величину (бюджет) между множеством объектов (клиентов) так, чтобы какой-то показатель (средний чек, доход) вырос. Отсюда вытекает как минимум две подзадачи:
Найти то, что влияет на целевой показатель. Например, доказать, что средний чек растёт быстрее у более доходных клиентов. Этот пункт (поиск воздействующих переменных и доказательство этой зависимости) не будет рассматриваться в данной статье.
Распределить заданную величину — перевести скаляр в вектор так, чтобы он соответствовал нашей потребности (росту целевой переменной).
Чтобы было проще понять, рассмотрим конкретный бизнес-пример.
«Категорийный кэшбэк» — вид программы лояльности, где клиенту предлагается каждый месяц выбирать набор категорий, за покупки в которых он будет получать повышенный кэшбэк. В ней мы хотим решить задачу «эффективного» распределения бюджета, когда при тех же затратах растут показатели доходности клиента. Другими словами, нам нужно каким-то образом разделить единый бюджет программы между клиентами так, чтобы общий показатель доходности вырос.
Как уже говорилось ранее, мы опускаем вопрос поиска переменной воздействия и принимаем за данность, что существует такая зависимость: чем выше доходность клиента, тем при прочих равных условиях аллокация на него большего бюджета принесёт больше дохода. Поэтому наша цель — распределить бюджет в соответствии с показателем доходности клиента.
Формально нам нужно из некоторой скалярной величины B (бюджета) получить вектор Vb, который по своему распределению будет соответствовать вектору доходности V (нам же нужно поощрить тех клиентов, у кого доходность выше), при этом среднее и максимальное значение вектора Vb будет зависеть от скалярной величины B.
На математическом языке постановку задачи можно записать следующим образом:
Дано:
Скаляр B ∈ R>0.
Вектор V = [V1, V2, …, Vn] ∈ Rn>0.
Константы: ϵ>0, Bavg>0, Bmax>0.
Требуется найти вектор Vb = [Vb1,Vb2, …, Vbn] ∈ Rn>0, такой что...
№1. Среднее значение:
№2. Максимальное и минимальное значения:
№3. Оптимальность (критерий сходства с V):
№4. Где D(⋅,⋅) — мера различия между распределениями Vb, V, например:
евклидова метрика (используется в статье),
расстояние Кульбака — Лейблера,
расстояние Васерштейна,
или любой другой подходящий критерий.
Основные переменные
Из общей постановки вытекают следующие базовые переменные.
№1. Бюджет — показатель, который хотим распределить
Обычно это скалярная величина, которая определяет общий предел и которую мы хотим распределить между объектами (в нашем случае клиентами).
В категорийном кэшбэке бюджетирование регулируется понятием «эффективная ставка» (ЭС) — отношение выплаченного кэшбэка к сумме трат клиента в целом. Увеличивая эффективную ставку мы увеличиваем показатель на выплаты.
Оперируя в проекте именно понятием «эффективной ставки» мы решаем две задачи:
с одной стороны, можем управлять бюджетом на клиентском уровне,
с другой стороны, нормируем привилегии на общий объем трат клиента, что позволяет учитывать различия в транзакционной активности клиентов и удерживать уровень воздействия вместе с ростом расходов клиента.
Такие показатели как Bavg, Bmax, Bmin зависят от размера бюджета (в нашем случае ЭС) и определяются исходя из целей, которые бизнес хочет достичь. Например, для задачи категорийного кэшбэка размер Bavg, Bmax, Bmin может принимать значение 0.3, 0.35 и 0.25 соответственно.
№2. Доходность клиента — показатель, по которому хотим распределить
В банке показатель доходности определяется показателем CLTV (customer lifetime value). CLTV — это сумма прибыли, которую банк ожидает получить от клиента за всё время его взаимодействия с банком.
CLTV — отличный показатель для ориентира на этапе распределения эффективной ставки: поощрение клиентов, которые пользуются продуктами банка, позволяет делать программу лояльности прозрачной и логичной (мы видим, что клиент приоритизирует наш банк и отвечаем ему тем же).
Подробнее о том, как считаем CLTV в Альфа-Банке:
Распределение доходности клиентов характеризуется длинными хвостами и сильной концентрацией точек в районе нуля. Разница между максимальным и минимальным значением может быть более 2 млн. рублей. При этом разница между 1 и 99 перцентилем может составлять уже менее 100 тыс. рублей.

Теперь рассмотрим варианты реализации.
Подход №1: линейное ранжирование
Основная предпосылка этого подхода — допущение, что достаточно сохранить порядок ранжирования клиентов по показателю доходности, а разницей в доходах клиентов можно пренебречь. То есть достаточно дать клиенту А больше, чем клиенту В (если доходность клиента А выше, чем у клиента В), при этом неважно насколько больше.
Одним из наиболее простых способов распределения ЭС является метод линейного ранжирования. Данный метод требует следующих параметров:
ERavg — заданное среднее значение эффективной ставки, которое мы ожидаем увидеть на всей популяции (аналог Вavg в общей постановке).
ERmin — минимально допустимое значение ЭС (аналог Вmin в общей постановке).
Вектор рангов клиентов R = (r1, r2,…, rn), где ri отражает уровень приоритета клиента (например, основанный на CLTV), при этом r1 < r2 ⇒ CLTV1 > CLTV2. Простыми словами R — это просто номер в иерархии среди всех клиентов: 1, 5 или 20 000, где чем меньше значение ранга, тем выше показатель CLTV.
Вектор начальных значений эффективной ставки ER=(er1, er2,…, ern), где изначально eri = ERavg для всех i.
Цель метода — скорректировать значения вектора ER с учётом рангов клиентов таким образом, чтобы среднее значение скорректированного вектора соответствовало
:
а клиент с максимальным рангом имел значение ЭС на уровне ERmin:
Для этого вводится вектор сдвига ΔER = (q1, q2,…, qn), который корректирует значения исходного вектора ставок ER:
Величина сдвига зависит от ранга клиента. Шаг отклонения от среднего значения определяется как:
где Rmedian — медианное значение ранга.
Отклонение для каждого клиента вычисляется как:
где Rmedian — медианное значение рангов.
Таким образом:
Если ri < Rmedian, то его эффективная ставка увеличивается.
Если ri > Rmedian, то его эффективная ставка уменьшается.
Давайте рассмотрим пример работы предложенного алгоритма
У нас есть 10 клиентов и два сегмента (mass и premium). Эффективные ставки (ЭС) задаются бизнесом на сегмент. Пусть бизнес хочет, чтобы средняя ЭС (ERavg) по всем клиентам mass была, допустим, 2.4% и не опускалась ниже (ERmin) 2%. Для premium эти значения соответствуют 4.5% и 4% соответственно. Распределять ЭС между клиентами мы будем согласно их значениям CLTV (см. Таблица 1).
Шаг 1. Добавляем ранг клиента (колонка R).
Шаг 2. Считаем шаг отклонения от среднего значения (Δ).
Шаг 3. Считаем медианное значение ранга Rmedian.
Шаг 4. Считаем отклонение от средней ЭС для каждого клиента (q).
Шаг 5. Прибавляем отклонение к средней ЭС и получаем итоговое значение ЭС на клиента (er).
def transform_distribution_v1(n, ERavg, ERmin):
"""
Параметры:
n: int — размер выборки;
ERavg: float — желаемое среднее значение;
ERmin: float или None — желаемый минимум (мягкое ограничение).
Возвращает:
Vb: np.ndarray — преобразованное распределение;
"""
r = np.arange(n)+1
Vb = np.full_like(r, ERavg, dtype=np.double)
r_median = n//2
diff = np.array([(ERavg-ERmin)/(r_median)]*n)
return Vb+(r-r_median-1)*diff
client_pin |
segment |
ERmin |
ERavg |
cltv |
R |
diff |
Rmedian |
q |
er |
AP123D |
mass |
2 |
2.4 |
-1000 |
7 |
0.133 |
4 |
-0.4 |
2 |
SD209E |
mass |
2 |
2.4 |
-823 |
6 |
0.133 |
4 |
-0.267 |
2.133 |
LS102E |
mass |
2 |
2.4 |
5 |
5 |
0.133 |
4 |
-0.133 |
2.267 |
CV321O |
mass |
2 |
2.4 |
12 |
4 |
0.133 |
4 |
0 |
2.4 |
MS932P |
mass |
2 |
2.4 |
142 |
3 |
0.133 |
4 |
0.133 |
2.533 |
AL012K |
mass |
2 |
2.4 |
1900 |
2 |
0.133 |
4 |
0.267 |
2.667 |
QC912L |
mass |
2 |
2.4 |
10324 |
1 |
0.133 |
4 |
0.4 |
2.8 |
MN321U |
premium |
4 |
4.5 |
512 |
3 |
0.5 |
2 |
-0.5 |
4.0 |
BV574I |
premium |
4 |
4.5 |
4703 |
2 |
0.5 |
2 |
0 |
4.5 |
FD728G |
premium |
4 |
4.5 |
54102 |
1 |
0.5 |
2 |
0.5 |
5.0 |
Этот метод позволяет перераспределять ЭС среди клиентов, сохраняя её среднее значение на уровне ERavg, что делает его простым и эффективным инструментом для адаптации ставок в зависимости от значимости клиентов.
Основной минус этого подхода — это одинаковый сдвиг относительно максимума/среднего без учета абсолютных разниц. То есть неважно как велика разница в целевой переменной между r1 и r2 клиентов, величина шага зависит только от допустимого отклонения от среднего и количества клиентов в выборке.
Подход №2: ранжирование с сохранением распределения целевой переменной
Суть этого подхода в попытке нормировать целевое распределение и ограничить его снизу допустимыми значением (и сверху, если требуется). Так как распределение целевой переменной почти никогда не имеет идеального нормального распределения, то добиться строгого соответствия сразу двум условиям (попадание в среднее и ограничение снизу/сверху при сохранении формы самого распределения) почти невозможно. Следовательно, формализуем задачу следующим образом.
Пусть дан вектор, который соответствует распределению целевой переменной (в нашем случае V (вектор распределения CLTV)). Необходимо найти такой вектор Vb (вектор с эффективными ставками), чтобы:
1) Новое распределение соответствовало заданному Vb:
2) Среднее значение распределения Vb должно быть строго равно ERavg:
3) Стараемся приблизится снизу и сверху к заданным ограничениям ERmin и ERmax :
Минимизируем отклонение значений от ERmin и ERmax, но не нарушаем первые два условия.
Решение
№1. Преображаем вектор V так, чтобы среднее стало ERavg:
№2. Масштабируем с учетом заданных границ:
Если softness = 0, то масштабирование не применяется ( scale=1 ).
Если softness = 1, масштабируем точно под желаемые границы.
№4. Вводим параметр смещения.
№5. Итоговое преобразование.
def transform_distribution_v2(V, ERavg, ERmin=None, ERmax=None, softness=0.1):
"""
Параметры:
V: np.ndarray — исходное распределение.
ERavg: float — желаемое среднее значение.
ERmin: float или None — желаемый минимум (мягкое ограничение).
ERmax: float или None — желаемый максимум (мягкое ограничение).
softness: float — коэффициент мягкости ограничений (0–1).
Возвращает:
Vb: np.ndarray — преобразованное распределение
"""
Vb = V - np.mean(V) + ERavg
V_min, V_max = np.min(V), np.max(V)
ERmin = ERmin if ERmin is not None else V_min
ERmax = ERmax if ERmax is not None else V_max
scale = (ERmax - ERmin) / (V_max - V_min + 1e-8)
shift = softness * (ERmin - V_min * scale)
scale = 1 + softness * (scale - 1)
Vb = Vb * scale + shift
Vb = Vb - np.mean(Vb) + ERavg
return Vb
Пример
Воспользуемся функцией выше и обновим значение er в нашем примере, softness примем равным 1.
er = transform_distribution_v2(cltv, ERavg, ERmin, ERmax, softness=1)
client_pin |
clientsegment |
ERmin |
ERavg |
cltv |
er |
AP123D |
mass |
2 |
2.4 |
-1000 |
2.222 |
SD209E |
mass |
2 |
2.4 |
-823 |
2.235 |
LS102E |
mass |
2 |
2.4 |
5 |
2.293 |
CV321O |
mass |
2 |
2.4 |
12 |
2.294 |
MS932P |
mass |
2 |
2.4 |
142 |
2.303 |
AL012K |
mass |
2 |
2.4 |
1900 |
2.427 |
QC912L |
mass |
2 |
2.4 |
10324 |
3.022 |
MN321U |
premium |
4 |
4.5 |
512 |
4.10 |
BV574I |
premium |
4 |
4.5 |
4703 |
4.218 |
FD728G |
premium |
4 |
4.5 |
54102 |
5.140 |
Подход №3*
По сути, это вариация подхода 2.
В этом подходе мы стремимся максимально сохранить исходное распределение и минимизировать отклонения от ограничений на среднее и максимально/минимально допустимые значения, переходя к задаче оптимизации.
Например, условие этой оптимизационной задачи можно задать через такую функцию потерь:
при этом,
Vb — новое (искомое) распределение эффективных ставок,
n — длина исходного вектора,
Vi — исходное распределение доходности клиентов,
λ — величина штрафа,
ERmin, ERmax — ограничения на итоговый вектор.
и оптимизируем по:
Здесь мы используем приближение по евклидовой метрике (MSE):
Но можно использовать любые другие метрики схожести.
Такую задачу можно решить, используя библиотеку scipy и методы оптимизации оттуда, например:
import numpy as np
from scipy.optimize import minimize_scalar
def transform_distribution_v3(V, ERavg, ERmin=None, ERmax=None, softness=1):
"""
Параметры:
V: np.ndarray — исходное распределение.
ERavg: float — желаемое среднее значение.
ERmin: float или None — желаемый минимум (мягкое ограничение).
ERmax: float или None — желаемый максимум (мягкое ограничение).
softness: float — коэффициент мягкости ограничений (0–1).
Возвращает:
Vb: np.ndarray — преобразованное распределение
"""
V_centered = V - V.mean()
n = len(V)
# Если границы не заданы — просто возвращаем аффинный сдвиг
if ERmin is None and ERmax is None:
return y_centered + ERavg
# Нормализация soft-параметра
alpha = np.clip(softness, 0.0, 1.0)
def loss(s):
Vb = V_centered * s + ERavg
mse_term = np.sum((Vb - V)**2)
penalty_min = penalty_max = 0.0
if ERmin is not None:
penalty_min = np.sum(np.maximum(0, ERmin - Vb)**2)
if ERmax is not None:
penalty_max = np.sum(np.maximum(0, Vb - ERmax)**2)
penalty = penalty_min + penalty_max
return (1 - alpha) * mse_term + alpha * penalty
# ищем s > 0; разумно ограничить диапазон
res = minimize_scalar(loss, bounds=(1e-6, 1e3), method='bounded')
s_opt = res.x
return V_centered * s_opt + ERavg
Это метод позволяет лучше, чем предыдущие сохранить форму исходного распределения, однако он довольно грубо нарушает ограничения на минимальные и максимальные значения (особенно при малом количестве данных и при softness<1). Сравним выходы всех трех методов
Сравнение
Давайте сравним результаты, полученные разными подходами. Для этого рассчитаем векторы er1, er2, er3 тремя подходами.
n = 7
ERavg, ERmin, ERmax = 2.4, 2, 2.8
V = [-1000,-823, 5, 12, 142, 1900, 10324]
er1 = transform_distribution_v1(n, ERavg, ERmin)
er2 = transform_distribution_v2(V, ERavg, ERmin, ERmax, softness=1)
er3 = transform_distribution_v3(V, ERavg, ERmin, ERmax, softness=0.99997)
client_pin |
cltv |
er1 |
er2 |
er3 |
diff_cltv |
diff_er1 |
diff_er2 |
diff_er3 |
AP123D |
-1000 |
2 |
2.222 |
2.195 |
0 |
0 |
0 |
0 |
SD209E |
-823 |
2.133 |
2.235 |
2.209 |
177 |
0.066 |
0.013 |
0.014 |
LS102E |
5 |
2.266 |
2.293 |
2.277 |
828 |
0.066 |
0.058 |
0.068 |
CV321O |
12 |
2.4 |
2.294 |
2.277 |
7 |
0.066 |
0 |
0.001 |
MS932P |
142 |
2.533 |
2.303 |
2.288 |
130 |
0.066 |
0.009 |
0.011 |
AL012K |
1900 |
2.666 |
2.427 |
2.431 |
1758 |
0.066 |
0.124 |
0.144 |
QC912L |
10324 |
2.8 |
3.022 |
3.119 |
8424 |
0.066 |
0.595 |
0.688 |
diff_* — содержит прирост значения относительного предыдущего по порядковому номеру значению.
diff_er_cltv — истинная разница в доходности между клиентами.
diff_er* — полученная разными подходами разница в доходности).
er* — подход к расчету er (er1 - первый, рассмотренный нами подход).
На графике ниже представлена визуализация табличных значений (выведены колонки diff_er_cltv, diff_er1, diff_er2, diff_er3).

Видно, что подходы 2 и 3 (diff_er2 и diff_er3) лучше повторяют динамику изменения целевой переменной, чем подход 1. То есть подходы 2 и 3 позволяют в большей мере сохранить разницу между итоговыми значениями соседних точек нового и оригинального распределений.
Для того, чтобы сравнить применимость метода для большего количества данных, построим графики преобразованных значения распределения y:


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