Учимся на наших ошибках 📈

Как Python, scikit-learn, Logistic Regression и Looker объединились, чтобы помочь нашим менеджерам по продажам найти «иголку в стоге сена».

Это небольшая статья, предназначенная как для аналитиков продаж, так и для практиков, которые, возможно, хотят изучить потенциальное использование машинного обучения в своих рабочих процессах. Здесь вы найдете фрагменты кода, примеры и уроки, которые я извлек из внедрения модели классификации в наши процессы продаж, Qubit.

Контекст

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

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

Суть.

Можем ли мы построить контролируемую модель машинного обучения, чтобы извлечь пользу из наших исторических данных о продажах?

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

Я должен заявить, что за кулисами здесь, в Qubit, было множество людей, которые давали отличные советы и рекомендации на протяжении всего этого проекта, без которых это было бы невозможно. Это было в дополнение к большому количеству отличных онлайн-ресурсов (цитируемых ниже).

(Эта статья не будет охватывать широту и понимание ML, для этого есть множество статей, написанных лучше! Я также предполагаю, что у вас уже предустановлен Python 3 в некоторой степени.)

Какой алгоритм я должен использовать?

Исходя из моего опыта и понимания, для задач такого типа обычно сначала исследуют эффективность минимально жизнеспособного продукта (MVP). В этом контексте используются хорошо цитируемые алгоритмы с минимальным или отсутствующим проектированием функций или предварительной обработкой данных, чтобы понять, где находится «нижний предел» в отношении точности и точности модели. Мой MVP включал алгоритмы логистической регрессии, дерева решений и случайного леса, потому что целевая переменная была двоичной. Чтобы статья оставалась целенаправленной, была выбрана двоичная логистическая регрессия, и вот несколько ключевых ресурсов, которые определенно помогли:

Книги

Интернет-ресурсы

Средние статьи

Гильдия аналитиков

  • Здесь, в Qubit, у нас есть внутренняя гильдия аналитиков, которая обеспечивает открытую и откровенную среду для отчетов о разработке модели и получения отзывов (привет всем участникам! 👊🏻).

У меня есть "Что и почему для того, как ..."

Qubit принял Looker для внутренней бизнес-аналитики в 2017 году, что позволило мне объединить API Looker и хранилище данных Qubit внутри Jupyter Notebooks, открыв доступ к одной из библиотек машинного обучения Python - scikit-learn.

Распространенное предположение:

«…« размер исторических сделок, которые мы проводим, слишком мал », а« согласованность в определенных областях »слишком слабая, чтобы построить что-либо, что могло бы сработать».

Чтобы разобраться с вышеизложенным и выяснить, может ли контролируемый алгоритм машинного обучения принести пользу, я предпринял следующие шаги:

  1. Предварительная обработка данных: загрузка данных, функции масштабирования, условное обозначение и категориальные функции горячего кодирования.
  2. Обучение + тест: разделение обучающих данных, загрузка алгоритма логистической регрессии scikit-learn, установка гиперпараметра и сохранение модели на диск.
  3. Производительность: кривые обучения, проверки и ROC для сравнения с матрицей неточностей.
  4. Поделиться: распространить результаты модели по Looker.

Эта статья, конечно же, не является исчерпывающим описанием процесса, который был / повторяется для дальнейшей работы с моделью!

Возможности покрытия

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

  • Скорость реализации: с точки зрения возраста и времени, потраченного на этапе продажи возможности.
  • Вовлеченность торгового представителя: с точки зрения изменений в ключевых полях возможностей, таких как «дата закрытия» и чистое пройденное расстояние (через канал продаж).
  • Вовлеченность HubSpot: мы используем HubSpot в качестве маркетингового инструмента, который содержит эмпирическую оценку, которую мы можем агрегировать и использовать для выявления возможностей.
  • Местоположение сделки: географическое положение сделки и представителя имеют исторические показатели выигрыша, которые могут дать контекст.
  • Размер сделки: в зависимости от типа вашего бизнеса SaaS и вашей стратегии выхода на рынок, это может быть полезной функцией.
  • Тип возможностей: мы в Qubit продаем несколько типов продуктов, разработанных с учетом различных циклов продаж.
  • Контекст продаж: в Qubit мы держим большой объем контекста в рамках торговых структур, таких как MEDDPICC, которые оказались полезными функциями.

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

1. Предварительная обработка данных

В этом разделе будет рассказано, как я использовал API Looker, функцию Pandas ‘pandas.read_sql_query’ и SQLAlchemy для оптимизации процесса.

Загрузить библиотеки

Загрузить данные

Благодаря приведенным ниже функциям я теперь могу импортировать последние данные из Looker или из представлений MySQL всего в несколько строк.

‘query_id’ можно найти на вкладке Запросы в панели администратора Looker (Документация Looker).

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

Примечание, новая функция Looker «объединить результаты» пока не работает с API, а пример query_id относится только к нашему экземпляру.

# Looker API
age_from_creation = get_data('147670')
deal_size = get_data('138578')
hubspot = get_data('146956')
# Data Warehouse
sql = "select * from pipe_qualifier_data"
days_in_stage_df = pd.read_sql_query(sql, data_warehouse)

Импутация и категоризация

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

# Impute NaNs caused by merging (categorically and continuously)
df.feature_X.fillna(value="Unknown", inplace=True)
df.feature_Y.fillna(value=0, inplace=True)
# Impute Negative Continious Variables
df.loc[df['days_at_0'] < 0, 'days_at_0'] = 0
df.loc[df['days_at_1'] < 0, 'days_at_1'] = 0
df.loc[df['days_at_2'] < 0, 'days_at_2'] = 0
# String Search Categorisation
def check_opp_name(oppname):
    if 'xxx' in oppname.lower():
        answer = 'x_opp'
    elif 'xxc' in oppname.lower():
        answer = 'x_opp'
    elif 'y' in oppname.lower():
        answer = 'y_opp'
    else:
        answer = 'non_xy_opp'
    return answer
df['opp_definition'] = df.opportunity_name.apply(check_opp_name)
# Create dummy variables (one hot encoding)
df = pd.get_dummies(df)
# Assign target variable and drop unwanted columns
df['target_variable'] = df['contract_stage']
df.drop(
    [
        'contract_stage_Contract_Signed', 'contract_stage_Lost',
        'key_deal_0'
    ],
    axis=1,
    inplace=True)
# Df tidy up
df.columns = map(str.lower, df.columns)
df = df.rename(
    index=str,
    columns={
        "meddpicc": "meddpicc_populated",
        "key_deal": "key_deal"
    })

Проблемы с данными

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

# SNS pair plot
g = sns.pairplot(
    df[[
        "age_of_opp", "close_date_movements", 
        "days_at_0", "pipe_exp",
        "sum_of_close_date_moves", "days_at_1", 
        "max_hubspot_score", "target_variable"
    ]],
    hue="target_variable",
    diag_kind="hist")
for ax in g.axes.flat:
    plt.setp(ax.get_xticklabels(), rotation=45)

Задача - это классическая ситуация «иголка в стоге сена», когда набор обучающих данных сильно несбалансирован, а целевой класс составляет ‹10% от общего числа. Это сочетается с тем фактом, что .shape фрейма данных вернул только 2010 строк.

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

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

2. Модельное обучение + тестирование

Входные данные были стандартизированы с использованием функции sklearn scale (), которая позволила улучшить производительность, поскольку выбросы оказывают значительное влияние на логистическую регрессию. В будущем будет рассмотрено использование «RobustScalar ()», поскольку он использует межквартильный размах для лучшего устранения влияния выбросов.

Учитывая небольшой размер выборки, было выбрано разделение поездов и тестов 70/30 - в идеале это должно быть 60/40 для дальнейшего тестирования способности модели к обобщениям. В будущем будет рассмотрено изучение использования «StratifiedKFold ()» и перекрестной проверки для увеличения размера тестовой / обучающей выборки и поддержания баланса классов.

# Splitting df into target and features
X = df[df.loc[:, df.columns != 'target_variable'].columns]
y = df['target_variable']
# Splitting features into a 70/30 train/test ratio
X_train, X_test, y_train, y_test = train_test_split(X, 
                                                    y, 
                                                    test_size=0.3)
# Scale inputs
X_train_scale = scale(X_train)
X_test_scale = scale(X_test)
# Logistic Regression C Value
logreg_scaled = LogisticRegression(C=1.0)
logreg_scaled.fit(X_train_scale, y_train)

Функция LogisticRegression Scikit-Learn имеет параметр ‘class_weight =’, который может автоматически применять вес, обратно пропорциональный частоте занятий.

# Logistic Regression Class Weighting
logreg_scaled_balanced = LogisticRegression(C=1.0,                 
                                            class_weight='balanced')
logreg_scaled_balanced.fit(X_train_scale, y_train)

Я исследовал это и обнаружил, что показатель отзыва для целевого класса значительно улучшился, однако точность также упала. Один из рисков искусственного уравновешивания несбалансированного класса - это чрезмерная подгонка. Я решил продолжить работу с несбалансированным набором.

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

Кривые обучения - хороший способ понять, как обучение (ось Y) меняется с увеличением опыта (ось X). График показывает, исходя из текущих функций, количество обучающих примеров кажется достаточным для сходимости.

Значения C 0,1 и 1,0 использовались для сравнения характеристик и уравновешивания риска переобучения - C = 1,0 использовалось в окончательной конфигурации модели.

Альтернативным методом может быть использование функции GridSearchCV в scikit-learn вместе с перекрестной проверкой для исчерпывающего подтверждения наилучшего значения гиперпараметра.

C: с плавающей точкой, по умолчанию: 1.0

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

Логистическая регрессия - Scikit-Learn

("источник")

Используя библиотеку pickle Python, можно было быстро и легко «сгладить» или «сериализовать» модель и ее текущую конфигурацию в файл .sav.

Модуль pickle реализует фундаментальный, но мощный алгоритм сериализации и десериализации структуры объекта Python. «Обработка» - это процесс, посредством которого иерархия объектов Python преобразуется в поток байтов, а «извлечение» - это обратная операция, посредством которой поток байтов преобразуется обратно в иерархию объектов.

# To Save
filename = 'pipe_qualifier_model.sav'
pickle.dump(logreg_scaled, open(filename, 'wb'))
# To Open
logreg_scaled = pickle.load(open('pipe_qualifier_model.sav', 'rb'))

Локальное сохранение на диск было полезно для производства и использования модели без необходимости постоянно перезагружать, обучать и тестировать модель.

3. Производительность

Кривые рабочей характеристики приемника («ROC») или площадь под кривыми («AUCROC») обычно используются для оценки производительности пороговых значений модели классификации машинного обучения. Часто сравнивают с классификатором с произвольным отгадыванием, показываемым прямой линией через (0,0) и (1,1).

logit_roc_auc = roc_auc_score(y_test,
                              logreg_scaled.predict(X_test_scale))
fpr, tpr, thresholds = roc_curve(y_test,
                       logreg_scaled.predict_proba(X_test_scale)             
                       [:,1])
plt.figure()
plt.plot(fpr, tpr, label = 'Logistic Regression Scaled (area = %0.2f)' % logit_roc_auc)
plt.plot([0, 1], [0, 1],'r--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve (scaled)')
plt.legend(loc="lower right")
plt.show()

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

Кривая ROC построена путем построения графика отношения истинно положительной скорости (TPR) к ложной положительной скорости (FPR) при различных настройках пороговых значений - Википедия

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

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

Модель получила удовлетворительную оценку за Точность по отношению к целевому классу (выигранные сделки), с оценкой отзыва, требующей некоторой доработки. Это неплохое начало (ИМО) с четкой областью внимания для улучшения модели.

# Confusion Matrix
y_pred = logreg_scaled.predict(X_test_scale)
print(classification_report(y_test, y_pred))

Проще говоря:

Точность:

  • 97% предполагаемых убытков были проигрышными.
  • 80% предполагаемых выигрышных сделок были выиграны.

Напомним:

  • 99% проигранных сделок в пределах данных были правильно классифицированы.
  • 50% выигранных сделок в рамках данных были правильно классифицированы.

Обратите внимание на столбец "Поддержка", в котором указан размер набора для тестирования.

Это часть процесса, которая повторяется на практике. Проверяются характеристики, а модели повторно обучаются, чтобы попытаться улучшить эмпирические характеристики.

На практике мы наблюдаем аналогичный уровень точности (оценка F1 = 62%), поскольку применяем эту модель во время «обкатки» в конце квартала продаж. Отзывы были особенно полезны для выбора новых функций, чтобы уловить контекст, который, возможно, отсутствует в настоящее время.

4. Поделиться

Чтобы внедрить эту модель, потребовались ощутимые результаты в виде новой эвристики или «извлеченных уроков» для руководства продажами.

«Были ли полезны извлеченные уроки?»

Самый эффективный способ поделиться новыми знаниями - это важность функции. Я использовал два метода, чтобы выделить важные особенности:

Коэффициенты логит-модели

# loop through columns and append the corresponding coefficient
w_con = []
for idx, col_name in enumerate(X_train.columns):    
    w_con.append([col_name, logreg_scaled.coef_[0][idx]])
print("Model Logit Coefficients")
# Return the real and absolute values for sorting
df = pd.DataFrame(w_con)
df["abs_coeffs"] = abs(df[1])
df.sort_values(by=["abs_coeffs"], ascending=False, inplace=True)
df = df.rename(index=str, columns={0: "Feature", 1: "Coefficient"})
df.head(10)

# Load up Random Forest
rf = RandomForestClassifier()
rf.fit(X_train_scale, y_train)
importances = rf.feature_importances_
# Sort by importance
std = np.std([tree.feature_importances_ for tree in rf.estimators_],
             axis=0)
indices = np.argsort(importances)[::-1]
feature_importances = pd.DataFrame(
    rf.feature_importances_, index=X_train.columns,
    columns=['importance']).sort_values(
        'importance', ascending=False)
print("Random Forest Classifier: Feature Importance")
feature_importances.head(10)

Используя не только размер весов, но и направление, я смог преобразовать внутренние веса в контекстуализированные выводы.

Вот синопсис:

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

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

Ешьте, спите, рейв, повторяйте

Разработка модели - это итеративная причина, по которой обратная связь жизненно важна для производительности. В ходе этого упражнения были получены некоторые впечатляющие результаты (не считая производительности модели), которые используются менеджерами по продажам здесь, в Qubit.

Была поставлена ​​задача принять во внимание полученные знания, подвергнуть сомнению особенности модели и повторить это упражнение, чтобы стремиться к совершенствованию. Я не хочу, чтобы это звучало банально, ведь изменение - одно из самых трудоемких дел в бизнесе, а не решение, к которому нужно относиться легкомысленно.

Думайте об этом проекте как о разведывательной миссии, в рамках которой было обнаружено значение уровня поверхности, но есть потенциал для большего. Применение правила 80/20; Я бы сказал, что большая часть ранней окупаемости инвестиций (времени) была признана, однако, с некоторыми трудностями получить вознаграждение возможно до того, как начнет действовать убывающая отдача.

Следите за другими краткими сообщениями о работе в области бизнес-аналитики, которую мы делаем в Qubit!

🚀