Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Этап 0: Начало пути
В реалиях современного мира, когда ведется повсеместная цифровизация и накопление данных обо всем и о каждом, возникает резонный вопрос, а как этими данными воспользоваться? Многие, наверняка, уже слышали о рекомендательных системах в сферах развлечения и продаж. Инвестиционные компании не стоят в стороне от современных тенденций в области Data Science и рекомендательных систем в частности. Так давайте рассмотрим, в чем особенности и какие этапы пришлось пройти одной крупной инвестиционной компании для того, чтобы разработать собственную рекомендательную систему для повышения эффективности кросс-продаж и что в итоге получилось.
Этап 1: Отрицание
Чем же отличается процесс продаж в инвестиционной компании от аналогичного процесса в других компаниях? Давайте посмотрим, что есть общего.
- Необходимость развития продаж после совершения клиентом первой покупки/сделки (начало стандартное, как и у всех).
- Продажа сопутствующих услуг, тут все довольно прозрачно (тоже все стандартно).
А теперь начинаются отличия и сложности.
- Обширный список услуг, не имеющих общих атрибутов и постоянно пополняемых новыми.
- Продажи осуществляются менеджерами, которые за время общения с клиентом (порой, не более пары минут) должны заинтересовать его новой услугой или даже продать ее.
- Менеджер может готовиться к звонку, изучить всю доступную информацию о клиенте, но тогда неминуемо упадет объем продаж.
- Наличие распределенной базы данных.
Исходя из вышеописанного была сформулирована цель: разработать систему, которая могла бы брать необходимую информацию о клиентах из распределенных баз и агрегировать ее в одном месте. После чего, на основе поведенческой модели клиента, выводить в карточку звонка список услуг, которые менеджер может предложить. Тем самым система должна быть нацелена на помощь менеджерам в повышении эффективности кросс-продаж и уменьшение вероятности недовольства клиента (ведь мало кто любит, когда ему часто звонят и предлагают ненужные или неактуальные услуги).
Этап 2: Гнев
Так как система нацелена на кросс-продажи, то в нее попадают клиенты, у которых есть хотя бы одна приобретенная услуга. Следовательно, к этому моменту о клиенте известен широкий пул информации: пол, возраст, город проживания, история его коммуникаций с менеджерами брокера, риск-профиль, приобретенные услуги, а также многие другие данные (в том числе знак зодиака уровень лояльности). В совокупности только общей информации более 100 различных атрибутов, что явно избыточно. Если говорить об атрибутах, связанных с услугами и инвестиционной деятельностью, то их наберется еще больше.
С целью сокращения атрибутов для каждого продукта была составлена матрица размером более 70х70 признаков, после чего для каждой вычислен ранг (максимальное количество линейно независимых строк) и корреляция между строками. Для этих целей использовались модули Pandas и NumPy Python. В результате матрицы были проверены на пересечение атрибутов и в итоге число уникальных данных было сокращено до 31, в том числе коррелируемых, например, риск-профиль клиента и связанные с ним величины доходности и просадки. Для каждого атрибута средствами Python проверена гипотеза о статистическом распределении, сезонности и тренде. На основании исследования атрибутов разработаны алгоритмы для очистки данных от аномалий и артефактов.
После того, как была проведена очистка и подготовка данных, мы развернули базу данных (БД) с четырьмя таблицами для агрегации и трансфера данных:
- данные о клиентах,
- данные о коммуникациях с клиентом,
- данные о сделках,
- данные по риск-профилю и его связь с продуктами.
Этап 3: Торг
После подготовки БД следовало решить, а какой тип рекомендательной системы использовать? Для повышения скорости интеграции и разработки было решено остановиться на базовых подходах – контентной или коллаборативной фильтрациях, тем более их легко модифицировать в другие системы фильтрации.
Как писалось ранее, система разрабатывается для широкого круга финансовых продуктов, которые могут не иметь общих атрибутов. Таким образом, от контентной фильтрации пришлось отказаться, так как для ее реализации требуется знать для каждого продукта набор определенных характеристик и его балльную оценку.
Дальнейшая разработка велась на основе коллаборативной фильтрации, которая формирует матрицы схожести для каждого клиента и в соответствии с заданным порогом определяет вероятность выбора того или иного продукта на основе поведения всей группы. Также коллаборативная фильтрация позволяет заполнять «пробелы» в данных. Такой вид фильтрации имеет несколько недостатков:
- Проблема с рекомендацией новых продуктов, так как их еще никто не покупал внутри группы.
- Эффект «серой овцы», т.е. рано или поздно появится клиент, который не войдет ни в одну из уже существующих групп, и система не сможет дать рекомендацию по продуктам, которые клиент еще не брал.
- Настройка порога для формирования критерия похожести клиентов.
После того как был выбран тип рекомендательной системы, для более гибкой настройки матриц похожести и преодоления эффекта «серой овцы» было решено использовать многоуровневые планы, т.е. факторы принимают значения от нуля до плюс бесконечности. Так как многие из выбранных атрибутов передаются в виде строковых данных, было выполнено шифрование в числовой эквивалент, например, значения атрибута «Пол» были закодированы следующим образом:
- жен. = 1
- муж. = 2
Это требовалось для того, чтобы можно было применять математические функции ко всему объему данных. Также для правильности выдачи рекомендаций и снижения влияния ряда атрибутов было выполнено нормирование с гипотезой о повышении.
Теперь, собственно, почему торг?
Необходимо было определиться с математическим аппаратом распределения клиентов по группам «похожести». Так как у нас получилось многомерное векторное пространство, то требовалось выбрать функцию, способную определять расстояние в многомерных векторных пространствах. К таким функциям относятся: косинусное расстояние, Евклидова метрика, Манхэттеновская метрика, коэффициент корреляции Пирсона и др.
Для простоты формирования матрицы и порога схожести была выбрана функция косинусного расстояния. Данная функция может принимать значения в диапазоне от 0 до 1, где 0 – это полное совпадение. Также для косинусного расстояния проще настраивать порог схожести, так как диапазон изначально четко определен и не требуется калибровка...
Этап 4: Депрессия
Однако, задав эту функцию в Python «в лоб», мы получили следующее:
dist_cosini = scipy.spatial.distance.cosine(user_one, user_two)
Из-за того, что эта функция в Python относится к классу «медленных», необходимое для формирования матрицы схожести время для 30000 клиентов в 31-мерном пространстве составит около двух суток, и это только тестовая выборка. Для нас это было недопустимо.
Первым, очевидным, решением было вычислить сходство одним из самых простых способов, через «l1-норму» (она же — Манхэттенское расстояние), либо через «l2-норму» (Евклидова метрика).
manhattan = round(numpy.linalg.norm(user_one - user_two, ord = 1), 4)
или
euclidean = round(numpy.linalg.norm(user_one - user_two, ord = 2), 4)
Данные варианты позволяют в разы повысить скорость вычислений. Казалось бы, следует выбрать один из вариантов и продолжить разработку системы. Однако, любой из этих вариантов приведет к тому, что необходимо проводить дополнительную калибровку порога попадания клиента в ту или иную группу. Так как это связано с тем, что «l1-норма» имеет значения в диапазоне от плюс-минус бесконечности, а «l2-норма» — от нуля до плюс бесконечности. На первый взгляд кажется, что берем Евклидову метрику, калибруем и точно вычисляем порог попадания в ту или группу и радуемся жизни…
И опять проблемы. Любая новая услуга или продукт, неминуемо «собьют» порог, аналогична ситуация и с закрытием продукта. Притом что такие действия происходят один-два раза в месяц.
И что же в этом случае делать?
Нормальные герои всегда идут в обход.
Руководствуясь этим правилом, а что нам мешает?
- «Медленные» и «несовершенные» функции.
- Великий и ужасный GIL (глобальный шлюз), который вшит в Python и не позволяет нам распараллеливать вычисления и использовать многоядерность.
Пойдем по порядку...
Откуда у косинуса расстояние
Вернемся к косинусному расстоянию. И почему оно столь популярно:
- Оно позволяет работать с разреженными данными (в парадигме продаж — большинство покупателей не покупает большинство продуктов).
- Безоговорочно выполняются все четыре аксиомы метрик.
- Является не-Евклидовым расстоянием, т.е. важным является свойство самих векторов, а не их положение в пространстве.
- Четкий диапазон значений: от 0 до 1, включительно.
А вот и формула:
Фактически, косинусное расстояние равно разности между единицей и косинусной мерой (косинусом угла между вектором А и B). Как вариант, можно записать это следующим образом:
import numpy as np
CosD = round(1 - (np.sum((user_one * user_two)) / (np.sqrt(np.sum(user_one**2)) * np.sqrt(np.sum(user_two**2)))), 4)
Как итог, скорость вычислений стала близка полученной для «l-норм», но все еще была недостаточно быстро.
Шлюз
GIL (Global Interpreter Lock) позволяет избегать конфликтов при обращении разных потоков к одному и тому же участку памяти. Фактически же, когда один из потоков «захватывает» GIL, остальные «замораживаются». Как итог, нет параллельных потоков — нет проблемы. А что же делать, когда хочется параллельных вычислений? Для этого можно воспользоваться модулем Numba.
Чтобы оптимизировать код Python, Numba берет байт-код из предоставленной функции и запускает на ней набор анализаторов. Байт-код Python содержит последовательность небольших и простых инструкций, что позволяет реконструировать логику функции из байт-кода без использования исходного кода. В результате Numba переводит байт-код Python в промежуточное представление LLVM (по синтексу похож на ассемблер и не имеет ничего общего с Python).
Таким образом, код принимает вид:
import numpy as np
from numba import njit
@njit
def dist_cosini(user_one, all_user):
user_point = []
for q in range(len(all_user)):
user_two = all_user[q]
CosD = round(1 - (np.sum((user_one * user_two)) / np.sqrt(np.sum(user_one**2)) / np.sqrt(np.sum(user_two**2))), 4)
user_point.append(CosD)
return user_point
Таким образом, импорт модуля Numba позволил «отвлечь» на себя GIL и оптимизировать выполнение циклов. В итоге удалось сократить время вычислений в 60 раз.
Этап 5: Принятие
После всех вычислений и окончательной настройки порога "похожести" клиентов (в общей сложности было получено 13 основных групп клиентов). Внутри этих групп для каждого клиента вычислена вероятность выбора того или иного продукта или услуги.
Для случаев, когда клиент использовал продукт два и более раза — появилась возможность определить примерную периодичность совершения сделок этого типа. Для этого использовался алгоритм на основе статистической медианы.
На базе данных о коммуникациях с клиентом были определены дни и даже конкретные часы, в которые тот или иной клиент «любит» звонить. Для этого использовался алгоритм на основе статистической моды с корректировкой с помощью статистической медианы.
На основе данных о некоторых продуктах, используя алгоритм коллаборативной фильтрации, были получены скорректированные данные о приемлемом уровне риска для клиента и рекомендуемых продуктах, соответствующих этим уровням.
В текущей версии разработки поставленная цель была полностью реализована. Результаты тестов показали повышение эффективности кросс-продаж. Как итог, система была интегрирована в CRM. Также информация о периодичности сделок клиентов и «любимый день и время» для звонков позволили оптимизировать время работы менеджеров с клиентами.
Заключение
Дальнейшая работа над системой будет связана с полным преодолением недостатков коллаборативной фильтрации:
- Рекомендация новых услуг (сейчас требуется некоторое время на преодоление критического порога количества сделок).
- Эффект «серой овцы» (пока обошли разбиением на 13 групп).
Для этого уже начаты работы по модификации системы в один из видов гибридной рекомендательной системы. Также ведутся работы по углублению работы рекомендательной системы со сложными продуктами.