Команда: Ки До, Рея Шах, Эшли Ван, Энтони Велтри
Постановка бизнес-задачи:
Наш клиент, «специализированный» продуктовый магазин, продает продукты питания с ограниченными возможностями распределения. Учитывая конкуренцию на рынке, этот магазин ищет способы увеличить свою прибыль. Нашей основной бизнес-целью является увеличение прибыли за счет увеличения выручки за счет внедрения программы распространения купонов.
Наша основная бизнес-цель по увеличению прибыли может быть далее разбита на две части. Во-первых, мы хотим понять, какие клиенты, скорее всего, примут наше купонное предложение, используя модели классификации. Понимание этой вероятности имеет решающее значение для качества обслуживания клиентов, поскольку клиенты расстраиваются, когда магазины присылают им купоны, которые они не будут использовать. Учитывая бюджет, наша вторая бизнес-цель — решить, какие клиенты получат купонные предложения, и ожидаемое увеличение доходов, которые мы можем получить от купонной программы. Для этого мы будем ранжировать наших клиентов на основе их вероятности принятия и увеличения прибыли, если клиенты примут купонные предложения. Затем мы выберем пороговое значение процента клиентов для отправки купонов, чтобы максимизировать ожидаемое увеличение прибыли.
Понимание данных
Наш набор данных содержит данные о покупателях специализированного продуктового магазина за последние два года. Набор данных содержит 32 переменные и 2241 наблюдение, каждое из которых содержит агрегированные данные об уникальном покупателе. Товары разделены на пять категорий: мясо, рыба, фрукты, вино и сладости. Для каждой категории данные показывают совокупную сумму, купленную каждым покупателем за все предыдущие покупки. Кроме того, у нас есть переменные, включая способы покупки: онлайн, в магазине или по каталогу, принятие купона: да или нет, а также многочисленные личные характеристики, такие как уровень образования, возраст, семейное/семейное положение и доход.
Затем наша команда может рассчитать вероятность того, что покупатель воспользуется купоном во время транзакции. Это делается с использованием информации о том, принимал ли клиент когда-либо предложение купона с момента своей первой покупки. В частности, мы можем понять склонность клиента использовать купон, учитывая доступную информацию.
# importing relevant packages # general packages needed to use dataframe, arrays and creating plots import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns # packages to prepare the data from sklearn.preprocessing import StandardScaler sc = StandardScaler() from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score # packages to run all the models from sklearn.cluster import KMeans from sklearn import tree from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.svm import SVC # packages to evaluate the models from sklearn.metrics import confusion_matrix, accuracy_score, roc_curve, auc df = pd.read_excel("/Users/rheashah/Documents/Data Science/marketing_campaign_1.xlsx") # dropping variables which will not be used df = df.drop(['MntGoldProds', 'Date_since', 'Month_since', 'Z_CostContact', 'Z_Revenue'], axis=1)
Подготовка данных
- Преобразование данных для создания категориальных переменных: образование, семейное положение, поколение
- Удаление выбросов: доход, #покупки
- Стандартизация данных: даты
- Создание новых переменных: использование ориентировочных цен для каждой категории продуктов питания для расчета дохода, полученного покупателем для магазина.
#%% Initial Data Cleaning ss = df.describe() # describing the dataset df['ID'].nunique() == len(df) # to confirm that all IDs are unique df.isnull().sum() # finding missing values df.Income = df.Income.fillna(df.Income.median()) # replacing missing income values with the median df['tenure'] = 2022 - df.Year_since # calculating how long the individual has been a part of the program df['num_purchases'] = df.NumCatalogPurchases + df.NumStorePurchases + df.NumWebPurchases # total number of purchases the individual has made df['percent_deals'] = df.NumDealsPurchases/df.num_purchases # calculating the deals a customer has accepted as a % of their total purchases # calculating the revenue the customer has generated using costs per lb for each food category df['revenue_cus'] = (df.MntFishProducts*6.99+df.MntFruits*1.29+df.MntMeatProducts*15.99+df.MntSweetProducts*4.79+df.MntWines*11.99)/df.num_purchases # defining marital status as a categorical variable df['Marital_Status'] = df['Marital_Status'].astype('category') # combining the 'alone' and 'single' categories df['Marital_Status'] = df['Marital_Status'].replace({'Alone': 'Single'}) # defining education as a categorical variable df['Education'] = df['Education'].astype('category') # combining the 'master' and '2n cycle' categories df['Education'] = df['Education'].replace({'2n Cycle': 'Master'}) # dropping all observations whose income is abnormally large indexNames = df[df['Income'] > 600000].index df.drop(indexNames , inplace=True) # dropping all observations whose expected revenue is abnormally large indexNames = df[df['revenue_cus'] > 30000].index df.drop(indexNames , inplace=True) # dropping all customers who made no purchases, but still have quantities for food categories specified indexNames = df[df['num_purchases'] < 1].index df.drop(indexNames , inplace=True) # dropping all customers born before 1925 indexNames = df[df['Year_Birth'] < 1925].index df.drop(indexNames , inplace=True) # dropping customers whose marital status is "absurd" indexNames = df[df['Marital_Status'] == "Absurd"].index df.drop(indexNames , inplace=True) # dropping customers whose marital status is "yolo" indexNames = df[df['Marital_Status'] == "YOLO"].index df.drop(indexNames , inplace=True) # resetting the indices for the dataframe df = df.reset_index() # creating empty arrays for the generation dummy variables gen_z = np.zeros_like(df.ID) millenial = np.zeros_like(df.ID) xennial = np.zeros_like(df.ID) x_gen = np.zeros_like(df.ID) baby_boom = np.zeros_like(df.ID) silent_gen = np.zeros_like(df.ID) # creating dummy variables for generation for i in range(len(df)): if df.Year_Birth[i] >= 1995: gen_z[i] = 1 elif df.Year_Birth[i] >= 1985: millenial[i] = 1 elif df.Year_Birth[i] >= 1979: xennial[i] = 1 elif df.Year_Birth[i] >= 1964: x_gen[i] = 1 elif df.Year_Birth[i] >= 1945: baby_boom[i] = 1 elif df.Year_Birth[i] >= 1925: silent_gen[i] = 1 # adding dummy variables to the dataframe df['gen_z'] = gen_z df['millenial'] = millenial df['xennial'] = xennial df['x_gen'] = x_gen df['baby_boom'] = baby_boom df['silent_gen'] = silent_gen educ = pd.get_dummies(df.Education) # creating dummy variables for education ms = pd.get_dummies(df.Marital_Status) # creating dummy variables for marital status ms = ms.drop('Absurd', axis=1) # deleting blank column ms = ms.drop('YOLO', axis=1) # deleting blank column df = pd.concat([df, educ, ms], axis = 1) # adding education and marital status dummies to the dataframe df = df.drop('index', axis=1) # dropping the duplicate index columns ss = df.describe() # describing the cleaned dataset #%% New df # sub-setting the dataset for clustering dfclus = df.drop(columns=['ID', 'Year_since', 'Recency', 'Marital_Status', 'MntMeatProducts', 'MntFishProducts', 'MntSweetProducts', 'MntWines', 'MntFruits','NumDealsPurchases', 'NumWebPurchases', 'NumCatalogPurchases', 'NumStorePurchases', 'NumWebVisitsMonth', 'tenure', 'num_purchases', 'revenue_cus', 'percent_deals', 'AcceptedCmp3', 'AcceptedCmp4', 'AcceptedCmp5', 'AcceptedCmp1', 'AcceptedCmp2', 'Complain', 'Response', 'Year_Birth', 'Education', 'Marital_Status']) # sub-setting the dataset for classification dfclass = df.drop(columns=['ID', 'Year_since', 'Marital_Status', 'Year_Birth', 'MntMeatProducts', 'MntFishProducts', 'MntSweetProducts', 'MntWines', 'MntFruits','NumDealsPurchases', 'NumWebPurchases', 'NumCatalogPurchases', 'NumStorePurchases', 'Single', 'millenial', 'num_purchases', 'revenue_cus', 'total purchases','Basic', 'AcceptedCmp3', 'AcceptedCmp4', 'AcceptedCmp5', 'AcceptedCmp1', 'AcceptedCmp2', 'Response', 'Education']) # calculating the total number of campaigns accepted sum_cmp = df.AcceptedCmp1 + df.AcceptedCmp2 + df.AcceptedCmp3 + df.AcceptedCmp4 + df.AcceptedCmp5 + df.Response promotion = [] # blank array for the target variable # creating the binary target # 1: customer has accepted at least one promotion; 0: customer has not accpted any promotions for i in range(len(sum_cmp)): if sum_cmp[i] >= 1: promotion.append(1) else: promotion.append(0) new_df = sc.fit_transform(dfclass) # scaling the data # splitting the dataset into train (80%) and test (20%) x_train, x_test, y_train, y_test = train_test_split(new_df,promotion,train_size=0.8, random_state = 0) def profit(tp, fp, fn, tn): rev = tp*10 + fp*(-0.25) + fn*(-5) + tn*0 # multiplying the cost-benefit matrix with the confusion matrix return rev
Исследовательский анализ данных
Взаимосвязи: типы товаров
Наша команда выделила один примечательный тип продукта — MntWines с MntMeatProducts, который имеет корреляцию 0,56, в то время как MntWines имеет низкую корреляцию с другими продукции в пределах от 0,38 до 0,40. Это указывает на то, что мясные продукты и вино дополняют друг друга, и, таким образом, если у нас есть клиент, который много покупает вино, мы также отправим купон на мясные продукты, чтобы стимулировать их покупать больше.
# creating a new dataframe with all food categories qty = df[['MntMeatProducts', 'MntFishProducts', 'MntSweetProducts','MntWines', 'MntFruits']] correlations2 = qty.corr() # finding the correlations between the quantity of each category bought sns.heatmap(correlations2, annot=True) # plotting the correlations plt.show()
Взаимосвязь: доход и выручка
Сосредоточившись на непрерывных переменных, наша команда решила посмотреть на распределение переменных. Мы обнаружили положительную корреляцию между доходом и прибылью, а это означает, что чем выше доход клиента, тем больше прибыли он принесет нашему магазину. Следовательно, если мы отправим купон, и клиент примет его, клиент, который принесет наибольшую прибыль, будет иметь более высокий доход.
plt.scatter(lrevenue_cus, linc) # plotting the relationship between the log of a customer's income and spending plt.show()
Кластерный анализ K-средних
Одной из бизнес-ценностей кластерного анализа является понимание существующей клиентской базы. Используя «Метод локтя», мы нашли оптимальное количество кластеров — семь. Объединив клиентов в оптимальное количество клиентов, команда менеджеров может принять решение о проведении дополнительных рекламных акций, которые не зависят от склонности клиента принять купон. Кластеризация также может дать представление о том, какой тип клиентов покупает конкретный продукт и чаще принимает купоны.
Мы обнаружили, что группы продуктов, из которых люди покупали, и количество купленных продуктов сильно различались для каждого кластера. Например, мы обнаружили, что кластер с самым низким средним доходом был также кластером с наименьшими покупками в каждой категории продуктов питания. Два кластера, приносившие наибольшую среднюю прибыль, также имели наибольшую вероятность иметь детей. Интересно, что в кластере с наименьшим количеством интернет-покупок также были более молодые люди, чем в других кластерах, что указывает на то, что купоны для более молодых покупателей должны быть погашены в магазине. Наконец, мы заметили, что в среднем кластеры, которые генерировали наибольшую прибыль, были также кластерами, в которых было заключено наименьшее количество сделок. Это указывало на то, что для того, чтобы максимально увеличить доход с помощью купонной программы, мы должны сосредоточиться не только на том, кто с наибольшей вероятностью примет купоны, но и на том, кто принесет наибольшую прибыль магазину.
#%% EDA- K-means Clustering to Understand the Data # finding optimal number of clusters using the elbow method new_dfclus = sc.fit_transform(dfclus) # scaling the data wcss_list= [] # initializing the list for the values of WCSS # using a for loop for iterations from 1 to 10 clusters for i in range(1, 11): kmeans = KMeans(n_clusters=i, init='k-means++', random_state = 0) # running the k_means algorithm kmeans.fit(new_dfclus) # finding the clusters wcss_list.append(kmeans.inertia_) # finding the within cluster sum of squares plt.plot(range(1, 11), wcss_list) # plotting the elbow curve plt.title('The Elbow Method Graph') plt.xlabel('Number of clusters(k)') plt.ylabel('wcss_list') plt.show() km = KMeans(n_clusters=7) # running the algorithm after the number of clusters has been decided y_predicted = km.fit_predict(new_dfclus) # fitting it to the dataset dfclus['cluster']= y_predicted # adding the clusters to the dataset dfclus.head() df['cluster']= y_predicted # adding the clusters to the dataset df.head() summary = df.groupby('cluster').mean() # summary of each cluster df.drop('cluster', inplace=True, axis = 1)
Моделирование
Основываясь на цели выявления клиентов, которые могут принять наши купоны, мы сформулировали ее как задачу классификации. Мы построили и сравнили пять моделей классификации, способных возвращать такие вероятности:
- Логистическая регрессия: проста в реализации и эффективна в обучении.
#%% Logistic Regression def logistic_reg(x_train, x_test, y_train, y_test): model = LogisticRegression(max_iter=1000, random_state = 0) # defining the model model.fit(x_train, y_train) # fitting the model to the training data y_pred = model.predict(x_test) # predicting y values for x_test cv = cross_val_score(model, x_train, y_train, cv=10) # 10-fold cross validation cv_mean = np.mean(cv) # finding the mean accuracy across all folds tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel() # creating the confusion matrix rev = profit(tp, fp, fn, tn) # calculating the expected increase in revenue due to the coupons return rev, cv_mean # returning the accuracy
- Логистическая регрессия с лассо: штрафует за переоснащение. Однако логистическая регрессия сталкивается с явной проблемой, поскольку она не может решать нелинейные задачи, и нереально, чтобы данные были действительно линейно разделимыми.
#%% Logistic Regression with Lasso def lasso_reg(x_train, x_test, y_train, y_test): model = LogisticRegression(penalty='l1', solver='liblinear', random_state = 0) # defining the model model.fit(x_train, y_train) # fitting the model to the training data y_pred = model.predict(x_test) # predicting y values for x_test cv = cross_val_score(model, x_train, y_train, cv=10) # 10-fold cross validation cv_mean = np.mean(cv) # fidning the mean accuracy across all folds tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel() # creating the confusion matrix rev = profit(tp, fp, fn, tn) # calculating the expected increase in revenue due to the coupons return rev, cv_mean # returning the accuracy
- Дерево решений: простое для визуализации и хорошо справляется с категориальными данными. Чтобы убедиться, что наше дерево не слишком сложное, мы установили максимальную глубину, используя точность перекрестной проверки (настройка гиперпараметров). Однако деревья решений подвержены серьезным изменениям из-за небольших различий в данных.
#%% Decision Tree Classifier def decision_tree(x_train, x_test, y_train, y_test): # defining pararmeters we would like to consider while hypertuning the tree # in this case, we are concerned with how deep the tree is # if the tree is too deep, there may be overfitting in the model tree_parameters = {'max_depth' : [None, 3, 4, 5, 8]} # defining a cross validation method to find the best parameter grid = GridSearchCV(tree.DecisionTreeClassifier(), tree_parameters) grid.fit(x_train, y_train) # fitting the model to the training data maxdepth = grid.best_params_["max_depth"] # finding the best parameter # defining the model with the best parameter found grid = tree.DecisionTreeClassifier(max_depth=maxdepth, random_state = 0) grid.fit(x_train, y_train) # fitting the model to the training data y_pred = grid.predict(x_test) # predicting y values for x_test # plotting the tree plt.figure(figsize=(50, 50)) tree.plot_tree(grid, filled = True, feature_names=dfclass.columns, rounded = True) cv = cross_val_score(grid, x_train, y_train, cv=10) # 10-fold cross validation cv_mean = np.mean(cv) # finding the mean accuracy across all folds tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel() # creating the confusion matrix rev = profit(tp, fp, fn, tn) # calculating the expected increase in revenue due to the coupons return rev, cv_mean # returning the accuracy
- Случайный лес: метаоценка, которая подходит для нескольких деревьев решений и использует среднее значение, чтобы сделать модель более точной без переобучения. Мы нашли идеальное количество деревьев, используя точность перекрестной проверки. Хотя эта модель работала хорошо, она требует большей вычислительной мощности и медленнее реализуется.
#%% Random Forest def random_forest(x_train, x_test, y_train, y_test): # defining parameters we would like to consider while hypertuning the random forest # in this case, we are concerned with how many trees are in the forest # if there are too many trees, there may be over-fitting in the model forest_parameters = {'n_estimators' : [10, 15, 20, 30, 100, 150, 200, 500]} # defining a cross validation method to find the best parameter model = GridSearchCV(RandomForestClassifier(), forest_parameters) model.fit(x_train, y_train) # fitting the model to the training data n_est = model.best_params_["n_estimators"] # finding the best parameter # defining the model with the best parameter found model = RandomForestClassifier(n_estimators=n_est, random_state = 0) model.fit(x_train, y_train) # fitting the model to the training data y_pred = model.predict(x_test) # predicting y values for x_test cv = cross_val_score(model, x_train, y_train, cv=10) # 10-fold cross accuracy cv_mean = np.mean(cv) # finding the mean accuracy across all folds tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel() # creating the confusion matrix rev = profit(tp, fp, fn, tn) # calculating the expected increase in revenue due to the coupons return rev, cv_mean # returning the accuracy
- SVM: эффективен в многомерных наборах данных и эффективно использует память. Этот алгоритм не обеспечивает прогнозируемых вероятностей, поскольку он рассчитывается с использованием 5-кратной перекрестной проверки. Поскольку эти оценки вероятности являются неотъемлемой частью следующего шага, ранжирования клиентов, машина опорных векторов в основном использовалась в качестве эталона для сравнения результатов случайного леса (и остальных алгоритмов).
#%% Support Vector Machine def support_vec(x_train, x_test, y_train, y_test): clf_svm = SVC(gamma='auto', random_state = 0) # defining the model clf_svm.fit(x_train,y_train) # fitting it to the training data y_pred = clf_svm.predict(x_test) # predicting y values for x_test cv = cross_val_score(clf_svm, x_train, y_train, cv=10) # 10-fold cv accuracy cv_mean = np.mean(cv) # finding the mean accuracy across all folds tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel() # creating the confusion matrix rev = profit(tp, fp, fn, tn) # calculating the expected increase in revenue due to the coupons return rev, cv_mean # returning the accuracy #%% Implemetation # running all the models log_reg = logistic_reg(x_train, x_test, y_train, y_test) # logistic regression las = lasso_reg(x_train, x_test, y_train, y_test) # logistic regression with lasso d_tree = decision_tree(x_train, x_test, y_train, y_test) # decision tree classifier r_for = random_forest(x_train, x_test, y_train, y_test) # random forest classifier svm = support_vec(x_train, x_test, y_train, y_test) # support vector machine # plotting the mean cv accuracies found for each model plt.figure() plt.bar(range(1,6),[log_reg[1],las[1], d_tree[1], r_for[1], svm[1]]) plt.xticks(range(1,6),['Logistic', 'Lasso','Decision Tree','Random Forest', 'Support Vector'], rotation=45) plt.ylabel("Accuracy") plt.ylim(0.7,0.8) plt.show() # plotting the expected increase in profit for each model plt.figure() plt.bar(range(1,6),[log_reg[0],las[0], d_tree[0], r_for[0], svm[0]]) plt.xticks(range(1,6),['Logistic', 'Lasso','Decision Tree','Random Forest', 'Support Vector'], rotation=45) plt.ylabel("Expected increase in Profit due to the Coupon Program") plt.show() # printing the mean cv accuracies for each model print('\nThe accuracies for the models are as follows', '\nLogistic Regression', log_reg[1], '\nLogistic Regression with Lasso', las[1], '\nDecision Tree Classifier', d_tree[1], '\nRandom Forest Classifier', r_for[1], '\nSupport Vector Machine', svm[1], '\n') # printing the expected increase in profit for each model print('\nThe expected increase in profit for the models are as follows', '\nLogistic Regression', log_reg[0], '\nLogistic Regression with Lasso', las[0], '\nDecision Tree Classifier', d_tree[0], '\nRandom Forest Classifier', r_for[0], '\nSupport Vector Machine', svm[0], '\n') # here, we can see that random forest is performing the best, so it is selected as the final model
Модель случайного леса дала нам наилучшие результаты, основанные как на точности вне выборки (OOS), так и на ожидаемой прибыли от купонной программы (подробнее см. в Оценке). Затем мы повторно обучили модель случайного леса на основе всего набора обучающих данных, чтобы получить окончательные параметры.
forest_parameters = {'n_estimators' : [10, 15, 20, 30, 100, 150, 200, 500]} model = GridSearchCV(RandomForestClassifier(), forest_parameters) model.fit(x_train, y_train) n_est = model.best_params_["n_estimators"] model = RandomForestClassifier(n_estimators=n_est, random_state = 0) model.fit(x_train, y_train) # predicting probabilities for x_test y_pred_prob = model.predict_proba(x_test)
Значение модели для нашей бизнес-задачи
Предписанный метод моделирования может решить бизнес-проблему, которую мы выявили на нескольких фронтах. Модель может учитывать различные структуры затрат в зависимости от сезонности или других экзогенных факторов, повышающих ценность бизнеса. Кроме того, наша модель может сообщить нам ожидаемую прибыль от купонной программы, где в некоторых случаях она может быть отрицательной. Это отрицательное значение будет информировать продуктовый магазин о том, будет ли реализация купонной программы стоить больше, чем они планируют заработать. Одним из случаев может быть то, что все их клиенты откажутся от купонов (крайний случай), и в этом случае будет рекомендовано не внедрять программу. Впоследствии, с вероятностью того, что клиенты примут купоны, заданные нашей моделью, мы сможем ранжировать клиентов. Дополнительная ценность здесь заключается в том, что покупатели получают купоны в зависимости от их приоритета в магазине. Другими словами, продуктовый магазин может выбирать, каким покупателям отправлять купоны, исходя из их склонности тратить [дополнительные] деньги в магазине. Рейтинг также служит еще одним способом сегментации клиентов для будущего анализа.
Оценка
Мы использовали две метрики, чтобы выбрать модель с наилучшей производительностью вне выборки: среднюю точность, полученную при 10-кратной перекрестной проверке с набором обучающих данных, и ожидаемую прибыль, полученную с помощью тестового набора данных. Мы использовали ожидаемую прибыль, поскольку оптимальной целью бизнеса является увеличение прибыли, и существует асимметрия между выгодой от правильных прогнозов и затратами на неправильные прогнозы.
Мы учитывали это с помощью весов в матрице затрат и выгод, которая наказывала ложноположительные и ложноотрицательные результаты для снижения риска. Ожидаемая прибыль, которую мы нашли путем умножения значений в матрице затрат и выгод и матрице путаницы, помогает оценить модель с точки зрения бизнеса. Как видно из приведенных ниже графиков, алгоритм Random Forest показал лучшие результаты с точки зрения обеих метрик оценки.
Развертывание
Наша модель Random Forest может использоваться для прогнозирования вероятности того, что покупатели примут купоны. Продуктовый магазин может ранжировать своих клиентов с помощью функции ранжирования, которая принимает два параметра: вероятность принятия купона и прибыль (нормализованная средняя прибыль, полученная каждым покупателем). Функция ранжирования для каждого клиента выглядит следующим образом:
На основе функции ранжирования каждый клиент получает рейтинговый балл, упорядоченный по возрастанию. Затем мы находим отдельных клиентов выше определенного процентиля (порога). Все клиенты выше этого процентиля получат купон. Для выбора порога мы нашли ожидаемый рост прибыли и точность модели с заданным порогом. Затем мы умножили ожидаемое увеличение прибыли и точность, чтобы получить оценку. Мы выбрали порог, который дает нам максимальный балл, а это означает, что порог будет иметь хороший уровень прибыли, но не за счет более низкой точности, которая может снизить успех кампании. Основываясь на наших тестовых данных, мы обнаружили, что отправка купонов 56% лучших клиентов приведет к максимальному баллу, который является максимальной прибылью (892,75 доллара США), обеспечивая при этом хороший уровень точности.
Последним шагом развертывания является определение типа купона, который получит каждый клиент. Существуют различные способы определения типа купона. Во-первых, мы можем отправить им купон со скидкой на продукты, которые они покупают в нашем магазине, что дает магазину наибольшую прибыль (исходя из предложенной нами структуры затрат). Это может быть связано с наиболее коррелированной категорией, чтобы попытаться убедить потребителя купить больше. Второй вариант — отправить им купон в зависимости от того, к какому кластеру они принадлежат, на основе кластеризации K-средних. Последний вариант — мы можем предоставить пакетный купон в зависимости от их наименее покупаемых и наиболее покупаемых товаров. Например, покупатель больше всего покупает мясные продукты, а меньше всего — рыбные. Поэтому мы отправим им купон, который гласит: Купите 2 фунта мяса, и мы дадим 20% скидку на рыбные продукты только за эту же покупку. Общая цель этой кампании — увеличить прибыль за счет аналитического выбора клиентов, получающих купоны, а затем принятия решения (на основе данных), какие типы купонов предлагать этим лицам/группам клиентов.
#%% Ranking the customers based on likelihood to use coupons as well as spending wt1 = 2 # weight for probability of using coupon wt2 = 1 # weight for spending # defining the profit for each customer pro = (df.MntFishProducts*6.99+df.MntFruits*1.29+df.MntMeatProducts*15.99+df.MntSweetProducts*4.79+df.MntWines*11.99)/df.num_purchases # information needed to normalize the customer's spending maxi = max(pro) mini = min(pro) rev_norm = 0 # initializing the variable rank = [] # blank array for the rank for i in range(len(y_pred_prob)): rev_norm = (pro[i] - mini)/(maxi - mini) # normalizing spending score = (y_pred_prob[i][1]*wt1 + rev_norm*wt2)/(wt1 + wt2) # weighted score rank.append(score) # plotting the score plt.plot(np.sort(rank)[::-1], color="red") plt.ylabel('Score based on Probability and Expected Profits') plt.xlabel('Customers') plt.show() #%% Prediction and Evaluation score_arr = [] # array to store calculated scores for i in np.arange(0, 1, 0.01): print('Threshold =', i) y_predicted = [] # blank array for predicted y values quartile = np.quantile(rank, i) # finding the score at the 20th quartile for i in range(len(y_pred_prob)): # y_predicted is 1 if the score is above the 20th quartile if rank[i] >= quartile: y_predicted.append(1) # y_predicted is 0 if the score is below the 20th quartile else: y_predicted.append(0) ypredicted = pd.Series(y_predicted) # converting the array to a pd series tn, fp, fn, tp = confusion_matrix(y_test, ypredicted).ravel() # confusion matrix for the model rev = tp*10 + fp*(-0.25) + fn*(-5) + tn*0 # multiplying the cost-benefit matrix with the confusion matrix sc = rev * accuracy_score(y_test, ypredicted) print('Score to identify the best quartile =', sc, '\n') score_arr.append(sc) # plotting the scores plt.plot(score_arr, color="red") plt.axvline(x = 44, color = 'b', label = 'axvline - full height') plt.ylabel('Score based on Accuracy and Expected Profits') plt.xlabel('Customers - No Coupon') plt.show()
Другие показатели оценки окончательной модели
#%% Final evaluation metrics y_predicted = [] # blank array for predicted y values quartile = np.quantile(rank, np.argmax(score_arr)/100) # finding the score at the 20th quartile for i in range(len(y_pred_prob)): # y_predicted is 1 if the score is above the 20th quartile if rank[i] >= quartile: y_predicted.append(1) # y_predicted is 0 if the score is below the 20th quartile else: y_predicted.append(0) ypredicted = pd.Series(y_predicted) # converting the array to a pd series print('The accuracy is', accuracy_score(y_test, ypredicted)) # printing the accuracy of the model tn, fp, fn, tp = confusion_matrix(y_test, ypredicted).ravel() # confusion matrix for the model print("The sensitivity is", tp/(tp + fn)) # printing the sensitivity of the model print("The precision is", tp/(tp + fp)) # printing the precision of the model # ROC curve and AUC y_pred_proba = model.predict_proba(x_test)[::,1] # probability of y = 1 fpr, tpr, _ = roc_curve(y_test, y_pred_proba) # finding the false positive rate and true positive rate print("AUC: ", auc(fpr, tpr)) # printing the AOC # plotting ROC curve plt.plot(fpr, tpr) plt.ylabel('True Positive Rate') plt.xlabel('False Positive Rate') plt.show() rev = tp*10 + fp*(-0.25) + fn*(-5) + tn*0 # multiplying the cost-benefit matrix with the confusion matrix print('The total expected increase in profits is', rev) # printing the expected increase in profits sc = rev * accuracy_score(y_test, ypredicted) # score of profits and accuracy print('Score to identify the best quartile =', sc, '\n')
Риск и другие факторы
Этическое соображение заключается в том, что мы ранжируем клиентов на основе их склонности принимать купон и их истории покупок, где чем больше денег потрачено, тем выше рейтинг. При постоянной вероятности выше оценивается тот, кто тратит больше. Однако обычно купоны нужны тем, у кого более низкий доход (и, следовательно, они не тратят много). Этическая проблема заключается в том, чтобы давать купоны людям, которые не нуждаются в них. Более серьезная потенциальная проблема, с которой мы можем столкнуться, связана с развертыванием этой модели для новых клиентов, чья история покупок не установлена. Это можно решить, используя их демографическую информацию для идентификации кластера и отправляя им купоны на основе моделей покупок в их кластере.