Как выглядит типичная домашняя работа по науке о данных и техническому консалтингу в MBB (McKinsey, Bain, BCG)? Давайте углубимся и перейдем к решению.
Такие фирмы, как McKinsey, Bain или Boston Consulting Group, являются мечтой для многих кандидатов. Их процесс найма известен тем, что он довольно сложный, долгий и с очень низким коэффициентом успеха. В этой статье я хочу поделиться (и решить) домашним заданием, которое я получил от MBB, состоящим из первого шага трехэтапного технического собеседования. Но без дальнейших церемоний давайте погрузимся!
Небольшое примечание: первый шаг был пройден с помощью решения, описанного в этой статье.
Введение:
Поскольку невозможно поделиться исходным домашним заданием, я подготовил резюме и изменил некоторые переменные.
Я участвую в программе трансформации, направленной на увеличение ARPU для ABC Telco. Поставленная передо мной задача состоит в том, чтобы повысить эффективность маркетинговой кампании ABC Telco за счет расширенной аналитики, в частности, конечная цель, поставленная передо мной, состоит в том, чтобы максимизировать прибыль от этой маркетинговой кампании, при этом прибыль определяется как:
pilot.csv
содержит информацию из исследования, проведенного в прошлом, каждая строка представляет клиента и его/ее характеристики, а также окончательное решение покупать/не покупать услугу. Кроме того, мы связались с A/B-тестированием, что видно в столбце treatment
. Этот файл можно использовать в качестве тренировочного набора для алгоритма, предназначенного для прогнозирования того, какие клиенты из CustomerBase.csv
с наибольшей вероятностью купят услугу с заданным набором характеристик.
Ограничения и нюансы:
- Средняя цена контакта с пользователем 5 единиц
- Результатом алгоритма является предоставление бинарного решения
- Покупка услуги приведет к увеличению пожизненной ценности клиента со скидкой в размере 100 единиц.
- Лечение -> 0 представляет контрольную группу, а 1 представляет контактное население.
- Количество звонков ограничено 1/4 всей клиентской базы
Задания:
- Максимизируйте прибыль от маркетинговой кампании
- Разработайте как минимум 2 модели (одна из которых является моделью ансамбля) и сравните их в соответствии с выбранными мной показателями.
План атаки:
Для выполнения этой задачи используется типичная структура науки о данных, весь анализ включает следующие этапы:
- 1.0 Исследовательский анализ данных (EDA)
- 2.0 Подготовка данных
- 3.0 Моделирование и оценка
- 4.0 Выводы
1.0 Исследовательский анализ данных (EDA)
Этот раздел направлен на понимание данных, содержащихся в наборе данных pilot
и наборе данных CustomerBase
. Он начинается с изучения предыдущего набора данных, проведения различных базовых статистических данных и анализа, оценки влияния A/B-тестирования и предоставления вспомогательных визуализаций.
Кроме того, поскольку объем данных позволяет, два отчета создаются с использованием пакета pandas_profiling
, очень полезного пакета для выполнения быстрого исследовательского анализа данных в кадре данных pandas.
pilot = pd.read_csv(‘../data/pilot.csv’) customer_base = pd.read_csv(‘../data/CustomerBase.csv’)
# Check for differences in purchase between control and treatment control = pilot[pilot[‘treatment’] == 0] treatment = pilot[pilot[‘treatment’] == 1] # Calculate total percentage (in relative terms) of customer who purchased for both groups control_pct = (len(control[control[‘purchase’] == 1]) / len(control) * 100) treatment_pct = ( len(treatment[treatment[‘purchase’] == 1]) / len(treatment) * 100) print(f'The pilot consists of {len(pilot)} customers') print( f'The total amount of customers contacted in treatment is {len(treatment)} customers') print(f'The total amount of customers in control is {len(control)} customers')
print( f’Absolute percentage of customer who purchased in pilot: {(len(pilot[pilot[“purchase”] == 1]) / len(pilot)) * 100}%’)
conversion_rates = pilot.groupby(‘treatment’)[‘purchase’] # Std. deviation of the proportion def std_p(x): return np.std(x, ddof=0) # Std. error of the proportion (std / sqrt(n)) def se_p(x): return stats.sem(x, ddof=0) conversion_rates = conversion_rates.agg([np.mean, std_p, se_p]) conversion_rates.columns = [‘conversion_rate’, ‘std_deviation’, ‘std_error’] conversion_rates.style.format(‘{:.3f}’) print(conversion_rates) plt.figure(figsize=(8, 6)) sns.barplot(x=pilot[‘treatment’], y=pilot[‘purchase’], ci=False) plt.ylim(0, 0.17) plt.title(‘Conversion rate by group’) plt.xlabel(‘Group’) plt.ylabel(‘Converted (proportion)’) plt.show()
Согласно некоторой базовой статистике, кажется, что есть небольшое увеличение числа клиентов, совершивших покупку в лечебной группе, однако, чтобы подтвердить это, следует провести более подходящий тест, такой как Z-тест.
Общие затраты, доходы и прибыль для пилота:
# cost of contacting a customer: total_costs = len(treatment) * 5 # discounted (future) revenues for all customer who purchased total_revenues = len(pilot[pilot[‘purchase’] == 1]) * 100 profit = total_revenues — total_costs print(f’Total cost of contacting customer is {total_costs} units’) print( f’Total revenues (incremental discounts) will be of {total_revenues} units’) print( f’Total profit for pilot is: {profit} units, based on {len(pilot)} customers of which {len(pilot[pilot[“purchase”] == 1])} purchased’)
Для customerBase
с учетом ограничений максимально возможная общая прибыль составляет:
max_amount_contactable_customers = (len(customer_base) * 0.25) cost_of_contact = 5 profit = 100 max_profit = max_amount_contactable_customers * \ profit — (max_amount_contactable_customers * 5) print( f’Maximum possible profit (assuming 100% of contacted customers purchase): {max_profit} units’)
Далее создается отчет с использованием pandas_profiling
, это помогает искать интересные закономерности в данных.
Судя по диагностике, видимой в отчете, есть несоответствие отдельных значений в столбце V19
:
pilot[‘V19’].value_counts().sort_values(ascending=False).plot(kind=’bar’)
customer_base[‘V19’].value_counts().sort_values( ascending=False).plot(kind=’bar’)
Это несоответствие может быть связано с разными факторами, например, пилотный проект проводился только для определенного набора клиентов (например, категории клиентов, которые владеют определенным устройством или попадают в определенный набор категорий).
Для того, чтобы понять, как поступить в этой ситуации, необходимо количественно оценить, сколько записей в базе клиентов затронуто:
print(f’Customer base contains {len(customer_base)} records.’) affected_records = len( customer_base[customer_base[‘V19’].isin(pilot[‘V19’].unique())]) print(f’Number of record affected by category mismatch is: {affected_records}’) print(f’{100 — (affected_records / len(customer_base) * 100)}%’) print(f’count of affected records: {len(customer_base) — affected_records}’)
Поскольку количество затронутых записей очень мало (59) для этой задачи, было решено пропустить и отфильтровать клиентскую базу по тем же категориям, которые присутствуют в пилотном наборе данных под переменной V19
.
customer_base = customer_base[customer_base[‘V19’].isin(pilot[‘V19’].unique())]
Еще одним важным шагом к пониманию данных является построение графиков распределения для каждой переменной, на этот раз путем наложения данных для обоих решений:
# distribution of features features = sorted( [i for i in pilot.columns if pilot.dtypes[i] != ‘object’]) plt.figure(figsize=(12, 28*4)) gs = gridspec.GridSpec(28, 1) for i, cn in enumerate(pilot[features]): ax = plt.subplot(gs[i]) sns.distplot(pilot[cn][pilot.purchase == 1], bins=50, label=’Purchased’) sns.distplot(pilot[cn][pilot.purchase == 0], bins=50, label=’Not purchased’) ax.set_xlabel(‘’) ax.legend() ax.set_title(‘histogram of feature: ‘ + str(cn)) plt.show()
Согласно диаграммам, некоторые переменные, такие как V7
, V20
, V10
, выглядят как гауссовые, что предполагает возможность, например, попробовать моделирование с использованием машины опорных векторов с ядром RBF.
Цель этой задачи — подогнать бинарный классификатор, полезно проверить дисбаланс классов в целевой переменной:
pilot[‘purchase’].value_counts().plot(kind=’bar’) print(pilot[‘purchase’].value_counts())
Целевая переменная имеет очень сильный дисбаланс, примерно 9:91.
Выводы:
На основании этого разведочного анализа данных можно сделать вывод, что:
- Существует дельта между экспериментальной и контрольной группой ~ 2,3%.
- Наборы данных содержат две категориальные и одну двоичную переменную в дополнение к непрерывной. Для целей моделирования это необходимо помнить.
- Максимальная (возможная) прибыль 2 256 250 единиц
- Присутствует сильный дисбаланс целевой переменной
2.0 Подготовка данных
Этот раздел направлен на подготовку данных перед подбором первой модели (ансамбль повышения стохастического градиента).
Для подбора модели используется пилотный набор данных. Однако столбец treatment
использоваться не будет, так как это не внутренняя функция данного пользователя, а случайно назначенная группа.
pilot = pilot.drop(‘treatment’, axis=1)
Как было показано в предыдущем разделе, в целевой переменной purchase
можно наблюдать сильный дисбаланс классов. Есть много способов справиться с дисбалансом классов (избыточная выборка, недостаточная выборка, связи Tomek, SMOTE...), и, рассматривая бинарный классификатор, обычно можно получить довольно высокую точность, просто предсказывая класс большинства, в то время как он не может не захватить класс меньшинства, что чаще всего является целью создания модели в первую очередь.
Кроме того, с точки зрения бизнеса было бы экономически целесообразно иметь более консервативный классификатор, то есть классификатор с высоким уровнем истинно положительных результатов по сравнению с ложноотрицательными. Обоснование этого выбора заключается в том, что контакт с потенциальными клиентами платный, и мы заинтересованы в контактах только с потенциальными клиентами с высокой вероятностью покупки.
Для этой задачи используется метод выборки SMOTE. Этот метод работает путем случайного выбора точек из класса меньшинства и вычисления k ближайших соседей для этой точки.
Для реализации этого алгоритма используется библиотека imblearn
, однако перед этим нужно разбить набор данных на признаки и цели, и закодировать категориальные переменные, получив дамми:
Обоснование этого выбора заключается в том, что эта методология не приводит к потере информации (избыточной выборке), однако необходимо отметить, что не всегда избыточная или недостаточная выборка является лучшим выбором: при работе с реальными данными некоторые явления по своей природе несбалансированы, следовательно, манипулирование целевым классом искажает реальную природу данных (например, обнаружение мошенничества, отток, дефолт по кредиту…)
# get dummies of categorical variables for both datasets pilot = pd.get_dummies(pilot, columns=[‘V2’, ‘V19’], drop_first=True) customer_base = pd.get_dummies(customer_base, columns=[ ‘V2’, ‘V19’], drop_first=True) # set aside 150 random records from pilot and remove them test_set = pilot.groupby('purchase').apply( lambda x: x.sample(frac=0.035, random_state=42)) # get list of indices to drop drop_indices = [i[1] for i in test_set.index] # reset index and shuffle test_set = test_set.reset_index(drop=True) test_set = test_set.sample(frac=1) # drop indices from original dataset (to avoid training on them) pilot = pilot.drop(drop_indices) # shuffle data pilot = pilot.sample(frac=1) # select all columns but purchase features = pilot.loc[:, pilot.columns != 'purchase'] targets = pilot['purchase'] # make sure input data has the same shape as customer base assert len(features.columns) == len(customer_base.columns)
Обычный, не всегда необходимый этап предварительной обработки — стандартизация, он не всегда необходим при работе с деревьями решений, но действительно рекомендуется для SVM.
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() features_scaled = scaler.fit_transform(features.values) # get a list of categorical columns indexes to fit SMONTENC cat_columns = [features.columns.get_loc(i) for i in features.columns if '_' in i] from imblearn.over_sampling import SMOTENC sm = SMOTENC(categorical_features=cat_columns, k_neighbors=10) X, y = sm.fit_resample(features_scaled, targets)
3.0 Моделирование и оценка
В целях обучения/проверки набор данных разбивается с использованием k-кратной перекрестной проверки, кроме того, применяется поиск по сетке для поиска оптимальных параметров, которые затем используются для подгонки модели ко всем обучающим данным. Эта стратегия была выбрана в соответствии с этим вопросом и цитируемой статьей.
Если бы не применялась избыточная (или недостаточная) выборка, более осознанный выбор был бы стратифицирован в k-кратном размере.
3.1 Моделирование — SVM
# get test data (unseen by the algorithm) X_test = scaler.transform(test_set.drop([‘purchase’], axis=1).values) y_test = test_set[‘purchase’].values
Для облегчения подбора нескольких моделей и быстрого повторения становится очень полезной специальная функция, которая возвращает лучшие параметры:
def fit_gs_model_w_report(model, scores, param_grid, X_train, y_train, X_test, y_test, n_jobs): ‘’’ Fit a model, generate classification report, and return best model params. ‘’’ for score in scores: print(“# Tuning hyper-parameters for %s” % score) print() model = GridSearchCV(model, param_grid, scoring=score, n_jobs=n_jobs) model.fit(X_train, y_train) print(“Best parameters set found on development set:”) print() print(model.best_params_) print() print(“Grid scores on development set:”) print() means = model.cv_results_[“mean_test_score”] stds = model.cv_results_[“std_test_score”] for mean, std, params in zip(means, stds, model.cv_results_[“params”]): print(“%0.3f (+/-%0.03f) for %r” % (mean, std * 2, params)) print() print(“Detailed classification report:”) print() print(“The model is trained on the full development set.”) print(“The scores are computed on the full evaluation set.”) print() y_true, y_pred = y_test, model.predict(X_test) print(classification_report_imbalanced(y_true, y_pred, target_names=[‘Not purchased’, ‘Purchased’])) print() return model.best_params_
Давайте подгоним первую модель, используя поиск по сетке, и интерпретируем выходные результаты:
scores = [“roc_auc”] param_grid_svc = {“kernel”: [“rbf”], “gamma”: [1e-2, 1e-3, 1e-4], “C”: [1000, 10000], “class_weight”: [‘balanced’] } svc_best_params = fit_gs_model_w_report(model=SVC(), scores=scores, param_grid=param_grid_svc, X_train=X, y_train=y, X_test=X_test, y_test=y_test, n_jobs=-1)
В соответствии с показателями точности и полноты для категории Purchased
, когда модель утверждает, что наблюдение действительно является покупкой, оно верно в 29% случаев и может обнаружить 40% покупок, однако важно отметить, что в наборе тестов сильный дисбаланс (160 отрицательных образцов и всего 15 положительных)
Теперь окончательная модель SVC с лучшими параметрами соответствует всему набору данных:
best_svc = SVC(probability=True) best_svc.set_params(**svc_best_params) best_svc.fit(X, y) y_scores_svc = cross_val_predict( best_svc, X, y, cv=10, method='predict_proba', n_jobs=-1) # since predict_proba returns predictions for each class, we need to focus on the positive class y_scores_svc = y_scores_svc[:, 1] precisions_svc, recall_svc, thresholds_svc = precision_recall_curve( y, y_scores_svc)
Чтобы сравнить этот классификатор с другой моделью, можно использовать показатель F1, однако показатель F1 отдает предпочтение классификаторам с аналогичной точностью и полнотой.
В идеале для этой задачи предпочтительнее иметь классификатор с немного меньшей полнотой, но более высокой точностью, а это означает, что клиент, с которым свяжется ABC Telco, имеет высокие шансы приобрести план.
Чтобы дополнительно настроить это, можно получить доступ к оценке решения модели, а затем можно выбрать соответствующий порог:
def plot_precision_recall_vs_threshold(precisions, recall, thresholds): plt.plot(thresholds, precisions[:-1], “b — “, label=”Precision”, linewidth=2) plt.plot(thresholds, recall[:-1], “g-”, label=”Recall”, linewidth=2) plt.xlabel(“Threshold”, fontsize=16) plt.legend(loc=”upper left”, fontsize=16) plt.ylim([0, 1]) plt.grid() plt.legend() plot_precision_recall_vs_threshold(precisions_svc, recall_svc, thresholds_svc)
На диаграмме видно, как точность падает ниже 90% при пороговом значении ~0,70.
Еще один распространенный метод выбора наиболее подходящего порогового значения — прямое построение точного графика в зависимости от полноты:
plt.plot(precisions_svc, recall_svc) plt.grid() plt.xlabel(‘Precision’) plt.ylabel(‘Recall’)
Судя по диаграмме, точность начинает резко падать примерно на уровне примерно 90% полноты. Это означает, что для этой задачи будет выбран компромисс между точностью и полнотой непосредственно перед этим падением.
# get the threshold at which precision is 90% threshold_90_precision_svc = thresholds_svc[np.argmax(precisions_svc >= 0.9)] ps_svc = precision_score(y, y_scores_svc > threshold_90_precision_svc) print(f’Precision: {ps_svc * 100}%’)
Однако классификатор со слишком низким показателем полноты также не очень полезен, поэтому необходимо также оценить показатель полноты после этой настройки:
rs_svc = recall_score(y, y_scores_svc > threshold_90_precision_svc) print(f’Recall: {rs_svc * 100}%’)
3.2 Классификатор повышения градиента
В этом разделе делается попытка подогнать вторую (в идеале более производительную) модель. Подобранная модель представляет собой классификатор повышения градиента, основанный на библиотеке LightGBM. Выполняется поиск по сетке для поиска лучших параметров, в частности, reg_alpha
, reg_lambda
, min_data_in_leaf
и num_leaves
добавляются как попытка предотвратить переобучение (очень часто встречается в древовидных методах).
scores = [“roc_auc”] param_grid_lgbm = { ‘learning_rate’: [0.5], ‘n_estimators’: [96], ‘num_leaves’: [64, 128], ‘boosting_type’: [‘gbdt’], ‘objective’: [‘binary’], ‘max_bin’: [416], ‘random_state’: [500], ‘colsample_bytree’: [0.32], ‘min_data_in_leaf’: [50], ‘subsample’: [0.3], ‘class_weight’: [‘balanced’], ‘reg_alpha’: [0.01, 0.05], ‘reg_lambda’: [0.01], ‘feature_fraction’: [0.2, 0.4] } lgbm_best_params = fit_gs_model_w_report(model=lgbm.LGBMClassifier(n_jobs=1), scores=scores, param_grid=param_grid_lgbm, X_train=X, y_train=y, X_test=X_test, y_test=y_test, n_jobs=-1)
Опять же, окончательная модель подбирается с использованием лучших параметров поиска по сетке:
best_lgbm = lgbm.LGBMClassifier() best_lgbm.set_params(**lgbm_best_params) best_lgbm.fit(X, y)
Судя по метрикам точности и полноты, эта модель работает лучше, однако необходимо иметь в виду, что использование этого семейства алгоритмов может привести к сильному переоснащению.
y_scores_lgbm = cross_val_predict( best_lgbm, X, y, cv=10, method=’predict_proba’, n_jobs=-1) # since predict_proba returns predictions for each class, we need to focus on the positive class y_scores_lgbm = y_scores_lgbm[:, 1] precisions_lgbm, recall_lgbm, thresholds_lgbm = precision_recall_curve( y, y_scores_lgbm) plot_precision_recall_vs_threshold( precisions_lgbm, recall_lgbm, thresholds_lgbm)
plt.plot(precisions_lgbm, recall_lgbm) plt.grid() plt.xlabel(‘Precision’) plt.ylabel(‘Recall’)
Порог для модели выбирается с использованием той же методологии, что и для SVC.
# get the threshold at which precision is 90% threshold_90_precision_lgbm = thresholds_lgbm[np.argmax( precisions_lgbm >= 0.90)] ps_lgbm = precision_score(y, y_scores_lgbm > threshold_90_precision_lgbm) print(f’Precision lgbm model: {ps_lgbm * 100}%’) rs_lgbm = recall_score(y, y_scores_lgbm > threshold_90_precision_lgbm) print(f’Recall: {rs_lgbm * 100}%’)
3.3 Оценка модели
В этом разделе обе модели сравниваются с оценочным набором, обсуждаются различия и нюансы между ними, выбирается окончательная модель и вычисляются прогнозы по клиентской базе.
СВК:
# get test data X_test = scaler.transform(test_set.drop([‘purchase’], axis=1).values) y_test = test_set[‘purchase’].values # scale data in the customer base customer_base_scaled = scaler.transform(customer_base) y_scores_svc = best_svc.predict_proba(X_test) # without threshold manipulation evs_cm_svc = confusion_matrix(y_test, y_scores_svc[:, 1] > 0.5) ConfusionMatrixDisplay(evs_cm_svc).plot()
evs_cmt_svc = confusion_matrix( y_test, y_scores_svc[:, 1] > threshold_90_precision_svc) ConfusionMatrixDisplay(evs_cmt_svc).plot()
Из матрицы путаницы видно, что корректировка порога в соответствии с желаемой целью может уменьшить количество ложных срабатываний, что приводит к снижению затрат.
Давайте посмотрим, как прогнозы относительно клиентской базы:
final_predictions_svc = best_svc.predict_proba(customer_base_scaled) constraint = int(len(customer_base) * 0.25) final_predictions_svc_df = pd.DataFrame( {'prediction': final_predictions_svc[:, 1] > threshold_90_precision_svc, 'probability': final_predictions_svc[:, 1]}) # sort final prediction by score to then apply the 1/4 constraint in a safe way final_predictions_svc_df = final_predictions_svc_df.sort_values( by='probability', ascending=False) final_predictions_svc_df = final_predictions_svc_df[final_predictions_svc_df['prediction'] == True] print( f'The SVC model would decide to contact {len(final_predictions_svc_df)} customers out of a maximum of {constraint}') The SVC model would decide to contact 11618 customers out of a maximum of 23735
Модель SVC предлагает контактировать с количеством клиентов, подходящим для колл-центра.
Чтобы смоделировать оценку того, сколько клиентов фактически купят план после того, как с ними свяжутся, из распределения выбирается биномиальная случайная величина, предполагающая вероятность 50%:
buy_decision_svc = binom.rvs(1, .5, size=len(final_predictions_svc_df)) final_predictions_svc_df[‘buy_decision_svc’] = buy_decision_svc # cost of contacting a customer: total_positive_predictions_svc = len(final_predictions_svc_df) total_buying_decisions_svc = len( final_predictions_svc_df[final_predictions_svc_df['buy_decision_svc'] == 1]) total_costs_svc = total_positive_predictions_svc * 5 # discounted revenues for all customer who purchased total_revenues_svc = total_buying_decisions_svc * 100 profit_svc = total_revenues_svc - total_costs_svc print( f'Total total cost of contacting selected customers is {total_costs_svc} units') print( f'Total revenues (incremental discounts) will be of {total_revenues_svc} units') print( f'Total profit for this scenario is: {profit_svc} units, based on {len(final_predictions_svc_df)} customers of which {len(final_predictions_svc_df[final_predictions_svc_df["buy_decision_svc"] == 1])} purchased')
# save business result to a table final_business_results = pd.DataFrame({ ‘model’: ‘SVC’, ‘total_cost_contacting_customers’: total_costs_svc, ‘total_customer_base’: len(customer_base), ‘contacted customers’: len(final_predictions_svc_df), ‘amount_of_customers_who_purchased’: len(final_predictions_svc_df[final_predictions_svc_df[“buy_decision_svc”] == 1]), ‘total_revenues’: total_revenues_svc, ‘profit’: profit_svc }, index=[0])
ЛГБМ:
# get test data X_test = scaler.transform(test_set.drop([‘purchase’], axis=1).values) y_test = test_set[‘purchase’].values # scale data in the customer base customer_base = scaler.transform(customer_base) evs_pred_lgbm = best_lgbm.predict_proba(X_test) # without threshold manipulation evs_cm_lgbm = confusion_matrix(y_test, evs_pred_lgbm[:, 1] > 0.5) ConfusionMatrixDisplay(evs_cm_lgbm).plot()
evs_cmt_lgbm = confusion_matrix( y_test, evs_pred_lgbm[:, 1] > threshold_90_precision_lgbm) ConfusionMatrixDisplay(evs_cmt_lgbm).plot()
Сравнение конечных показателей точности и полноты (после настройки порога) показывает, что вторая модель значительно увеличила полноту, в то время как точность осталась прежней. С другой стороны, на данном этапе может иметь смысл построить рядом с матрицей путаницы для обеих моделей:
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(18, 4)) fig.suptitle(‘SVC VS LGBM — Confusion matrix’) ConfusionMatrixDisplay(evs_cmt_lgbm).plot(ax=axs[0]) axs[0].set_title(‘LGBM’) ConfusionMatrixDisplay(evs_cmt_svc ).plot(ax=axs[1]) axs[1].set_title(‘SVC’)
final_predictions_lgbm = best_lgbm.predict_proba(customer_base_scaled)
Из рисунка видно, что модель LGBM действительно лучше фиксирует истинные положительные результаты, в то время как количество ложноотрицательных результатов уменьшилось с 12 до 9. С другой стороны, количество ложноположительных результатов немного увеличилось. Тем не менее, с точки зрения бизнеса, контакт с 4 дополнительными потенциальными клиентами определенно увеличит затраты на 20 единиц, но нацеливание еще на 3 потенциальных клиентов, которые действительно купят план, приведет к более высокой прибыли в долгосрочной перспективе.
Вот почему модель LGB используется в качестве окончательной модели.
final_predictions_lgbm_df = pd.DataFrame( {‘prediction’: final_predictions_lgbm[:, 1] > threshold_90_precision_lgbm, ‘probability’: final_predictions_lgbm[:, 1]}) # sort final prediction by score to then apply the 1/4 constraint in a safe way final_predictions_lgbm_df = final_predictions_lgbm_df.sort_values( by=’probability’, ascending=False) final_positive_predictions_lgbm_df = final_predictions_lgbm_df[ final_predictions_lgbm_df[‘prediction’] == True] print( f’The LGBM model would decide to contact {len(final_positive_predictions_lgbm_df)} customers out of a maximum of {constraint}’)
По-видимому, модель LGBM более щедра, чем модель SVC.
Смоделируйте окончательное решение из биномиального распределения:
buy_decision_lgbm = binom.rvs( 1, .5, size=len(final_positive_predictions_lgbm_df)) final_positive_predictions_lgbm_df[‘buy_decision_lgbm’] = buy_decision_lgbm # cost of contacting a customer: total_positive_predictions_lgbm = len( final_positive_predictions_lgbm_df[final_positive_predictions_lgbm_df[‘prediction’] == True]) total_buying_decisions_lgbm = len( final_positive_predictions_lgbm_df[final_positive_predictions_lgbm_df[‘buy_decision_lgbm’] == True]) total_costs_lgbm = total_positive_predictions_lgbm * 5 # discounted revenues for all customer who purchased total_revenues_lgbm = total_buying_decisions_lgbm * 100 profit_lgbm = total_revenues_lgbm — total_costs_lgbm print( f’Total total cost of contacting selected customers is {total_costs_lgbm} units’) print( f’Total revenues (incremental discounts) will be of {total_revenues_lgbm} units’) print( f’Total profit for this scenario is: {profit_lgbm} units, based on {len(final_positive_predictions_lgbm_df)} customers of which {len(final_positive_predictions_lgbm_df[final_positive_predictions_lgbm_df[“buy_decision_lgbm”] == 1])} purchased’)
Теперь можно создать итоговую таблицу для сравнения важных для бизнеса данных:
# save business result to a table final_business_results = final_business_results.append({ ‘model’: ‘LGBM’, ‘total_cost_contacting_customers’: total_costs_lgbm, ‘total_customer_base’: len(customer_base), ‘contacted customers’: len(final_positive_predictions_lgbm_df), ‘amount_of_customers_who_purchased’: len(final_positive_predictions_lgbm_df[final_positive_predictions_lgbm_df[“buy_decision_lgbm”] == 1]), ‘total_revenues’: total_revenues_lgbm, ‘profit’: profit_lgbm }, ignore_index=True)
Важно знать, что поскольку окончательное решение о покупке моделируется случайной функцией, приведенные выше оценки могут незначительно изменяться (хотя в целях воспроизводимости заявлено начальное значение).
В следующей таблице представлены предполагаемые затраты, доходы и прибыль:
Целью этой задачи является максимизация прибыли от этой кампании, поэтому модель LGBM была выбрана в качестве окончательной.
4.0 Выводы
Принимая во внимание, что эта задача выполнялась на ноутбуке, я не хотел использовать какой-либо слишком трудоемкий метод, такой как более обширный поиск по сетке. Я действительно считаю, что ключом к повышению точности этой модели является более продвинутая предварительная обработка данных при повторении различных методов предварительной обработки. Я почти сразу заметил, как нормализация действительно улучшила выходные данные модели.
Кроме того, доступ к большему количеству данных (+100 000 помеченных наблюдений) сделал бы эти ранее упомянутые методы более надежными.
возможные пути улучшения результата:
- наличие доступа к большей вычислительной мощности ускорит и упростит оптимизацию гиперпараметров.
- знание бизнес-значения функций поможет в выборе/интерпретации переменных
- более обширная предварительная обработка методом проб и ошибок (наряду с вычислительной мощностью для быстрой итерации)
- больше тренировочных данных
I have a newsletter 📩. Every week I’ll send you a brief findings of articles, links, tutorials, and cool things that caught my attention. If tis sounds cool to you subscribe. That means a lot for me.
Повышение уровня кодирования
Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:
- 👏 Хлопайте за историю и подписывайтесь на автора 👉
- 📰 Смотрите больше контента в публикации Level Up Coding
- 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"
🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу