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

Предполагая, что ваш бизнес продолжает стимулировать и оптимизировать действия, которые имеют наибольшее положительное влияние на создание повторных покупок, вы создаете больше представителей органического бренда, которые будут направлять друзей в ваш бизнес без каких-либо дополнительных затрат для вас. Кроме того, в зависимости от объемов покупок, совершаемых этими постоянными покупателями, это увеличит среднюю пожизненную ценность клиента. При всем постоянстве, чем выше средняя пожизненная ценность, тем больше возможностей для маневра вы должны потратить на привлечение новых клиентов по существующим и / или новым каналам. Затем вы можете использовать это, чтобы убедить своих инвесторов дать вам больше денег, чтобы они переехали в более прохладное офисное помещение, накормили своих сотрудников обедами и купили бильярдный стол побольше, чтобы «привлечь новые таланты».

В качестве отправной точки вам необходимо понять факторы на пути клиента, которые имеют большее значение для повышения вероятности того, что покупатель совершит повторную покупку, а затем: i.) Работать над оптимизацией этих факторов и ii.) Возможно Начните тестирование персонализированного электронного маркетинга для клиентов, которые с большей вероятностью станут постоянными клиентами, чтобы убедиться, что они снова купят продукты у вашего бизнеса - это, конечно, может включать понимание типов продуктов, которые им больше интересны.

Помня об этом контексте, я решил проанализировать набор данных Kaggle на бразильской платформе электронной коммерции Olist - с разделом исследовательского анализа данных, чтобы изучить и понять больше о самих данных, поведении пользователей и потенциально ценных тенденциях и машине обучающий / аналитический раздел, посвященный использованию алгоритма классификации для прогнозирования вероятности того, что покупатель совершит повторные покупки, на основе его прошлого поведения на платформе.

Исследовательский анализ данных

В процессе EDA я сосредоточился на ответах на следующие вопросы: i. Насколько эффективен каждый маркетинговый канал для Olist, глядя на коэффициенты конверсии для каждого канала, среднее время, необходимое каждому каналу для преобразования пользователей от первого контакта к «закрытие» потенциального клиента, ii.) медианное значение количества клиентов, исходящих из каждого канала, для большей ясности в отношении того, сколько денег должно быть выделено для привлечения клиентов на каждый канал, iii.) медианная разница во времени между датой покупки и датой, когда заказ доставляется клиенту по состоянию и iv.) разница во времени между фактическим сроком доставки и предполагаемым сроком доставки.

merged_list = pd.merge(closed_deals,mark_qualified,on='mql_id',how='outer').fillna(0)
frames = [merged_list,seller_data,order_list]
seller_merged = reduce(lambda left, right: pd.merge(left,right,on='seller_id',how='outer'),frames).fillna(0)more product a customer purchases the greater the the customer is customer purchases
#calculate different between won date and first contact date
seller_merged['time_to_close'] = pd.to_datetime(seller_merged['won_date']) - pd.to_datetime(seller_merged['first_contact_date'])
#extract days from time to close
seller_merged['days_to_close'] = seller_merged['time_to_close'].apply(lambda x: x.days)

Поскольку набор данных разбит на несколько таблиц в Kaggle, первым шагом, который я сделал, было объединение таблиц, выбрав полное внешнее соединение. Подобно SQL, полное внешнее соединение объединяет все данные из всех наборов данных. Слияние с этой и другими таблицами потребовало понимания схемы данных, выложенной на Kaggle.

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

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

#closing success rate by point of origin
seller_merged['closed'] =  np.where(seller_merged['won_date']!=0, "True","False")
#percentage of deals closed/not closed by point of origin
closed_or_not = seller_merged.groupby(['origin', 'closed']).agg({'closed': 'size'})
ax = closed_or_not.groupby(level=0).apply(lambda x: 100 * x / float(x.sum())).unstack().plot.bar(stacked=True)
for p in ax.patches:
    width, height = p.get_width(), p.get_height()
    x, y = p.get_xy()
    horiz_offset = 1
    vert_offset = 1
    ax.legend(bbox_to_anchor=(horiz_offset, vert_offset))
    ax.annotate('{:.0f} %'.format(height), (p.get_x()+.15*width, p.get_y()+.4*height))

Чтобы визуализировать коэффициент конверсии для потенциальных клиентов, проходящих через каждый канал и конвертирующихся в платящего клиента, я создал столбец, который возвращает логическое значение true или false в зависимости от того, содержит ли выигранная дата значение 0. За этим последовала лямбда-функция для разделите общее количество закрытых и незакрытых интересов на общее количество потенциальных клиентов, прошедших через каждый канал. Я вернул эти числа в виде процентов, умножив каждое число на сотню, и нанес эти числа на гистограмму с накоплением 100%.

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

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

Визуализация среднего времени, необходимого для закрытия сделки

#length of time to close deals by origin
closing_deals = seller_merged.query('closed == "True"')
medians =  seller_merged.query('closed == "True"').groupby('origin')['days_to_close'].median().values
median_labels = [f'{m} days' for m in medians]
pos = range(len(medians))
ax = sns.violinplot(x='origin',y='days_to_close',data=closing_deals)
for tick,label in zip(pos,ax.get_xticklabels()):
  ax.text(pos[tick],medians[tick]+0.5,median_labels[tick],
         horizontalalignment='center',size='xx-large',color='b',weight='semibold')
sns.set(rc={'figure.figsize':(20,15)})

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

Интересно, что визуализация показывает, что среднее время, необходимое для закрытия платного поискового запроса, составляет 7 дней, что является одним из самых низких показателей среди всех других каналов, в то время как социальные сети и электронная почта - это ошеломляющие 42 и 35 дней. Следует отметить, что 0 и unknown являются неизвестными каналами и были включены в эти визуализации только для полного представления всех данных, представленных в наборе данных, как есть.

Средняя ценность клиента по каналам

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

#average value by origin
geolocation = pd.read_csv(r"/Users/emmanuelsibanda/Downloads/olist_geolocation_dataset.csv")
customer_db = pd.read_csv(r"/Users/emmanuelsibanda/Downloads/olist_customers_dataset.csv")
orders_db = pd.read_csv(r"/Users/emmanuelsibanda/Downloads/olist_orders_dataset.csv")
#merge on zip code
geolocation = geolocation.rename(columns={'geolocation_zip_code_prefix':'zip_code'})
customer_db = customer_db.rename(columns={'customer_zip_code_prefix':'zip_code'})
seller_merged = seller_merged.rename(columns={'seller_zip_code_prefix':'zip_code'})
#crashing my ram
seller_customer = pd.merge(seller_merged,customer_db,on='zip_code',how='outer').fillna(0)

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

#unique identifiers for duplicate zero values
seller_customer['customer_id'] =seller_customer['customer_id'].mask(seller_customer['customer_id'] == 0 , seller_customer['customer_id'].eq(0).cumsum())
#currency conversions
seller_customer['usd_price'] = seller_customer['price'].apply(lambda x: x*0.26)
seller_db = pd.DataFrame(seller_customer.groupby(['origin','customer_id'])['usd_price'].sum()).reset_index()
ax_chart = seller_db.groupby('origin')['usd_price'].mean().plot.bar()
ax_chart.set_xlabel("Point of origin")
ax_chart.set_ylabel("Mean Customer Value")
for x in ax_chart.patches:
  ax_chart.text(x.get_x()+.04,x.get_height()+10,\
               f'${round(x.get_height(),2)}',fontsize=11,color='black',
               rotation=45)

Одна из проблем с набором данных заключалась в том, что многие идентификаторы customer_ids были записаны как нулевые, хотя предположительно это были разные пользователи. Простое использование функции groupby без исправления этой ошибки приведет к подсчету всех идентификаторов клиентов, равных нулю, как одного пользователя, что приведет к увеличению средних значений времени жизни клиентов. Чтобы исправить эту ошибку, я сделал предположение, что каждый идентификатор клиента, записанный как ноль, был уникальным пользователем, и добавил уникальные идентификаторы, чтобы различать каждый ноль - путем добавления n к каждому нулю, где n был числовым порядком, в котором появляется ноль, например первый ноль будет 0 + 1, второй ноль будет 0 + 2 и т. д.

Для простоты понимания я преобразовал стоимость транзакции, потраченную на каждую покупку, из бразильских реалов в доллары США. На момент анализа курс составлял примерно 0,26, при этом 1 бразильский реал составлял примерно 26 центов США. Это было сделано для удобства чтения, поскольку цифры казались намного выше, чем были на самом деле.

Затем я вернул среднюю общую сумму, потраченную клиентами на канал, или среднюю пожизненную ценность клиента на канал.

Хотя средние значения не являются надежными по отношению к выбросам, возвращение медианы в этом контексте не представлялось целесообразным, поскольку большинство потенциальных клиентов не приводили к конверсиям и, следовательно, имели бы нулевые значения, что дало бы нулевую медиану для многих каналов. Глядя на представленную диаграмму, можно увидеть, что платный поиск дает значительную среднюю пожизненную ценность клиентов - примерно на 401% больше, чем средняя ценность клиентов, приходящих из социальных сетей. Однако следует отметить, что неизвестной переменной в этом анализе являются затраты на привлечение клиентов. Может случиться так, что привлечение клиентов через платный поиск будет стоить значительно дороже. Реальная ценность - это разница между средней продолжительностью жизни клиента и средними затратами на привлечение клиентов на канал: чем больше разница, тем эффективнее канал.

Время, необходимое для выполнения заказа

#time to deliver orders
seller_customer = pd.merge(seller_customer,orders_db,on='customer_id',how='outer').fillna(0)

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

def reformat_date(df, column):
  df[column] = pd.to_datetime(df[column])
reformat_date(seller_customer,'order_purchase_timestamp')
reformat_date(seller_customer,'order_approved_at')
reformat_date(seller_customer,'order_delivered_carrier_date')
reformat_date(seller_customer,'order_delivered_customer_date')
reformat_date(seller_customer,'order_estimated_delivery_date')
#calculating time delta
seller_customer['purchase_to_approval'] =(seller_customer['order_approved_at']-seller_customer['order_purchase_timestamp']).apply(lambda x: x.total_seconds()//3600)
seller_customer['time_to_completion'] =(seller_customer['order_delivered_customer_date']-seller_customer['order_purchase_timestamp']).apply(lambda x: x.total_seconds()//3600)
seller_customer['estimated_actual'] =(seller_customer['order_estimated_delivery_date']-seller_customer['order_delivered_customer_date']).apply(lambda x: x.total_seconds()//3600)
#filter negative values
seller_customer2 = seller_customer[((seller_customer.purchase_to_approval > 0) & (seller_customer.time_to_completion > 0))]

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

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

Затем я визуализировал разницу во времени между расчетным сроком доставки и фактическим сроком доставки - предположение о том, что время доставки постоянно недооценивается, может отрицательно сказаться на удовлетворенности клиентов.

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

Прогнозирование постоянных клиентов

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

#column of number of purchases made
seller_customer['no_purchases'] = seller_customer.groupby('customer_id')['customer_id'].transform('count')
#create column for repeat and non repeat purchases
seller_customer['repeat'] = seller_customer['no_purchases'].apply(lambda x: 1 if x > 1 else 0)

Готовясь к этой задаче классификации, я начал с определения клиентов, которые совершали повторные покупки, используя логическое значение 1 или True и 0 для неповторения (False).

Без категорий на числовые

#categorical to numerical
seller_customer['customer_city'] = seller_customer['customer_city'].astype('category')
seller_customer['customer_city'] = seller_customer['customer_city'].cat.codes
#origin to numerical
seller_customer['origin'] = seller_customer['origin'].astype('category')
seller_customer['origin'] = seller_customer['origin'].cat.codes
#lead behaviour profile
seller_customer['lead_type']=seller_customer['lead_type'].astype('category')
seller_customer['lead_type']=seller_customer['lead_type'].cat.codes
#removing purchase dates with value 0
seller_customer_clean = seller_customer[seller_customer['order_purchase_timestamp'] != 0]
#sort orders by purchase date
seller_customer_sorted = seller_customer_clean.sort_values(by='order_purchase_timestamp')
#return first purchase made by each customer
seller_customer_sorted.drop_duplicates(subset ="customer_id",keep = 'first', inplace = True)

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

Матрица корреляции

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

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

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

Построение модели логистической регрессии

Хотя алгоритмы регрессии обычно лучше всего подходят для прогнозирования непрерывных переменных, логистическая регрессия возвращает вероятность возникновения двоичного события. Хотя технически мы можем использовать алгоритм линейной регрессии для той же задачи, проблема заключается в том, что с помощью линейной регрессии вы можете провести прямую линию наилучшего соответствия по вашей выборке. Использование этого для задач классификации на основе вероятности возникновения события может потенциально дать вам результаты, если вероятность ниже 0 или больше 1 (лучше всего объясняется в специализации Стэнфордское машинное обучение на Coursera).

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

В контексте моего набора данных модель логистической регрессии будет возвращать вероятность того, что клиент является повторным клиентом, если вероятность превышает 0,5, клиент будет классифицирован как повторный клиент. Я решил использовать этот алгоритм из-за простоты его интерпретации, и он мне показался очень подходящим для этой задачи.

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score 
from sklearn.model_selection import train_test_split
#defining features and target
X = seller_customer[['origin','review_score']].values
y = seller_customer['repeat'].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
logisticModel = LogisticRegression()
#training data with logistic regression model
logisticModel.fit(X_train,y_train)
#predicting with models
predicts = logisticModel.predict(X_test)
#test model accuracy
from sklearn.metrics import classification_report
#print(classification_report(y_test,predicts))
print("Accuracy:", accuracy_score(y_test, predicts))

Отчет о классификации в моем коде отображает несколько показателей, включая: точность, оценку f1, отзыв и поддержку для каждого класса. В целом отчет используется как метод оценки качества прогнозов, сделанных алгоритмом классификации. Точность показывает процент правильных прогнозов, сделанных моделью (точность модели / оценка точности) в этом контексте для каждого из двоичных результатов. Отзыв возвращает оценку способности классификатора находить все положительные примеры для каждого из результатов, в то время как оценка f1 и поддержка возвращают процент положительных прогнозов, которые оказались верными, и количество истинных ответов, лежащих в пределах каждого результата, соответственно. . Переводя это на интуитивно понятные результаты, модель может с точностью около 91,05% предсказать, будет ли покупатель повторным покупателем, на основе предположений, сделанных в ходе моего анализа (предположений, определяющих, что такое повторный покупатель).

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

Перекрестная проверка

Одним из возможных недостатков метода разделения тест / поезд является возможность получения слишком оптимистичной оценки точности. Несмотря на то, что 30% моих данных хранятся как тестовые данные для оценки точности моей модели, предполагая, что 30% приравниваются к 2000 строкам, возможно, что моя модель будет работать очень хорошо на одном случайном наборе из 2000 строк и плохо на другом случайно выбранные 2000 строк. Чтобы получить полное представление о том, насколько точна моя модель, я мог бы провести некоторую форму перекрестной проверки. Это потребует запуска модели на разных подмножествах моих данных. Для простоты применения я выбрал перекрестную проверку k-кратных складок. С помощью этого метода я бы разделил свои данные на k различных складок, где k означает количество групп, на которые будет разделен образец данных. Из исследования я прочитал, что k = 10 или k = 5 были выброшены как хорошее практическое значение для k. Поскольку я решил использовать десять сгибов, k-сгибы будут работать с использованием 9 подмножеств для обучения моих данных, исключая 10-е в качестве набора для проверки и повторяя этот процесс 10 раз, при этом набор проверки будет меняться с каждой итерацией. Процесс заканчивался тем, что я вычислял средние оценки точности на каждой итерации.

Важно отметить, что k-кратная перекрестная проверка не всегда практична. K-fold включает выполнение чего-то похожего на метод разделения теста / поезда k раз, что делает его более дорогостоящим для больших наборов данных.

from sklearn.model_selection import KFold
kfold = KFold(n_splits=10,shuffle=True,random_state=0)
#running 10 fold cross validation and returning scores in list
scores = []
for train_index, test_index in kfold.split(X):
  print("Train Index:", train_index, "\n")
  print("Test Index:", test_index)
  X_train,X_test,y_train,y_test = X[train_index],X[test_index],y[train_index],y[test_index]
  logit.fit(X_train,y_train)
  scores.append(logit.score(X_test,y_test))
#printing out averaged accuracy as percentage
print("Accuracy: {}%".format(np.mean(scores)*100))
print("Variance: {}".format(np.var(scores)))
###results###
>>> Accuracy: 91.00565131889124%
>>> Variance: 2.6873619430516173e-06

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

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

Так заключает мое berceuse.

Не стесняйтесь обращаться с любой критикой, отзывами, советами и / или бесплатными билетами на Avengers Endgame в моем Twitter @Emmoemm