Команда: Ки До, Рея Шах, Эшли Ван, Энтони Велтри

Постановка бизнес-задачи:

Наш клиент, «специализированный» продуктовый магазин, продает продукты питания с ограниченными возможностями распределения. Учитывая конкуренцию на рынке, этот магазин ищет способы увеличить свою прибыль. Нашей основной бизнес-целью является увеличение прибыли за счет увеличения выручки за счет внедрения программы распространения купонов.

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

Понимание данных

Наш набор данных содержит данные о покупателях специализированного продуктового магазина за последние два года. Набор данных содержит 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)

Подготовка данных

  1. Преобразование данных для создания категориальных переменных: образование, семейное положение, поколение
  2. Удаление выбросов: доход, #покупки
  3. Стандартизация данных: даты
  4. Создание новых переменных: использование ориентировочных цен для каждой категории продуктов питания для расчета дохода, полученного покупателем для магазина.
#%% 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')

Риск и другие факторы

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