Прогнозирование конверсии посетителей интернет-магазина в покупателей с помощью машинного обучения

В этой статье я расскажу о своем опыте работы с анализом и прогнозированием конверсий веб-сайта в рамках моего участия в соревнованиях Kaggle Data Challenge.

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

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

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

Но как определить тех, кто мог бы купить? Если мы узнаем, что некоторые из них провели некоторое время на страницах, где действовала скидка, сколько из людей, не совершивших покупку на этой неделе, являются существующими покупателями, а сколько — новыми посетителями?

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

Ну что-ж, приступим…

Эксплораторный анализ данных (EDA)

В ходе эксплораторного (разведывательного) анализа данных я обнаружил несколько проблем.

Несбалансированность классов Покупателей и Непокупателей

Датасет с посещениями сайта оказался достаточно объемным. Обучающий и тестовый наборы данных состоят из 8630 и 3700 записей посещений соответственно. Предикторная переменная Revenue имеет значение 1 для посетителей, ставших покупателями, и 0 для тех, кто просто покинул сайт, без покупки.

Надо отметить, что коэффициент конверсии этого сайта — очень достойный — 15,4%. Однако набор данных очень несбалансирован, т. е. из 8630 посетителей в обучающем наборе данных только 1335 стали покупателями.

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

Допустим, у нас есть алгоритм, который просто помечает каждую запись как принадлежащую к классу большинства. В нашем случае из каждых 100 записей 85 будут принадлежать к классу большинства (Non-Revenue), и поэтому алгоритм, который просто помечает каждую запись как не приносящую дохода, легко достигнет точности 85%. Однако недостатком алгоритма является то, что он будет иметь точность 0% для записей, принадлежащих к классу меньшинства, поскольку он будет классифицировать каждую Revenue запись как Non-Revenue.

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

ROC AUC расшифровывается как Receiver Operating Curve — площадь под кривой. Показатель ROC AUC колеблется между 0 и 1, и высокое значение означает, что модель хорошо обучилась в разделении положительных и отрицательных примеров. В Data Science ROC AUC — это широко используемая метрика для оценки эффективности моделей, особенно в случае несбалансированных наборов данных.

Атрибуты данных

Набор данных содержит 17 атрибутов-предикторов, которые являются числовыми (непрерывными и дискретными) и категориальными, идентификатор клиента и выходную переменную выручки, которая принимает значение 0 или 1.

Имя атрибута Тип данных Примечание
ID: Customer ID String Уникальный идентификатор посетителя
Administrative Numerical Discrete Количество административных страниц сайта, которые посмотрел посетитель
Administrative Duration Numerical Continuous Как долго посетитель находился на административных страницах
Informational Numerical Discrete Типы посещенных информационных страниц
Informational Duration Numerical Continuous Как долго посетитель находился на информационных страницах
Product Related Numerical Discrete Количество товарных страниц, которые посмотрел посетитель
Product Related_duration Numerical Continuous Сколько времени посетитель провел на товарных страницах
Exit Rate Numerical Continuous Показатель выхода каждого клиента на странице. Означает как скоро клиент покидает страницу, меньшее значение может говорить о более коротком пребывании, а большее — о более длительном.
Bounce Rate Numerical Continuous Процент сеансов где пользователи посетили только 1 страницу сайта
Page Values Numerical Continuous Среднее количество страниц сайта, которые посетил пользователь до совершения сделки
Special Day Numerical Discrete Близость времени посещения сайта к особому дню. Варьируется от 0 до 1, с шагом 0,2, при этом 1 может означать, что дата наиболее близка или попадает на особый день, например «черную пятницу».
Month Categorical 10 месяцев: февраль, март, май, июнь, июль, август, сентябрь, октябрь, ноябрь, декабрь
Operating System Categorical 8 систем (от 1 до 8)
Browser Categorical 12 типов интернет-браузеров — от 1 до 13
Region Categorical Регион, в котором находится клиент — от 1 до 9
Traffic Type Categorical 19 типов интернет-трафика — от 1 до 20
Visitor_Type Categorical 3 типа: Вернувшийся посетитель, Новый посетитель, Другое
Weekend Categorical Посещал ли клиент сайт в выходные дни или нет (0=FALSE, 1= TRUE)

Выходная (целевая) переменная только одна.

Имя атрибута Тип данных Примечание
Revenue Categorical Привело ли посещение клиента к получению дохода (0=FALSE, 1= TRUE)

Отсутствующие значения

Я отсортировал обучающий набор данных, чтобы проверить, нет ли у некоторых записей недостающих атрибутов. В результате было найдено 8 записей с 8 отсутствующими атрибутами. Поскольку эти 8 записей составляют менее 0,1% от тренировочного набора данных, я решил просто удалить их из набора данных. Аналогично, в тестовом наборе данных было 6 записей, у которых не было этих 8 атрибутов, и я их тоже удалил.

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

Хотя это и приблизительный расчет, учитывая, что пропущенных записей очень мало (~0,1%) и картина пропущенных атрибутов последовательна, я посчитал, что в данном случае такой подход вполне приемлем. Если бы количество записей с пропущенными значениями было значительным, скажем 5-10% от набора данных, то такая стратегия удаления пропущенных значений могла бы привести к потере ценной информации. И тогда потребуется альтернативная стратегия, например, вменение недостающих значений.

Читайте также:  Поиск инсайтов в данных веб-аналитики с помощью Python и Pandas

Недостающие атрибуты обучающего и тестового наборов данных приведены в таблицах ниже:

Отсутствующие значения в обучающем датасете

Рис 1: Отсутствующие значения в обучающем датасете

Отсутствующие значение в тестовом датасете

Рис 2: Отсутствующие значение в тестовом датасете

Инсайты из эксплораторного анализа данных

Вот что я заметил в ходе EDA, проведенного на обучающих данных:

Для каждого категориального атрибута я построил график процентного соотношения посетителей Revenue (0) и Non-Revenue (1), и сравнил его со средним значением посетителей Revenue, т. е. ~15%. Таким образом я хотел проверить, существуют ли какие-то атрибуты, конкретные значения которых приводят к непропорционально высокому или низкому уровню посетителей с Revenue. Я использовал термины revenue visitors/revenue generating visitors и customers как взаимозаменяемые.

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

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

Коэффициент конверсии в зависимости от количества посещенных страниц на сайте

Рис 3: Коэффициент конверсии в зависимости от количества посещенных страниц на сайте

Далее я обнаружил еще один интересный инсайт. Я построил график клиентов по количеству посещенных ими административных страниц, и заметил, что нет никакой особой тенденции в процентном соотношении клиентов (Revenue=1) и количества просмотренных ими административных страниц, за исключением того факта, что из тех посетителей, которые не просмотрели ни одной административной страницы (3994 человека), только 9% (362 человека) совершили покупку. Таким образом, количество административных страниц выглядит несущественным фактором для совершения события Revenue, за исключением этого небольшого наблюдения.

Взаимосвязь просмотра административных страниц сайта и совершения покупки

Рис 4: Взаимосвязь просмотра административных страниц сайта и совершения покупки

Вот краткое изложение моих выводов в результате исследовательского анализа данных (EDA):

Число просмотров страниц: Существует сильная положительная корреляция между количеством посещенных страниц ювелирного магазина и коэффициентом конверсии. Люди, посетившие более 20 страниц, имеют особенно высокий коэффициент конверсии (>70%) по сравнению с теми, кто посетил 20 или менее страниц (~10%). Это говорит о том, что если сделать сайт более привлекательным, сделать так, чтобы люди оставались на нем дольше и просматривали больше страниц с ювелирными изделиями, то и покупать они будут больше. Это корреляция, которая не может означать причинно-следственную связь, т. е. те посетители, которые в конечном итоге совершают покупку, возможно, посещают больше страниц с ювелирными изделиями. Тем не менее эта гипотеза выглядит наиболее очевидной для проверки в рамках планируемых владельцем сайта А/B тестов.

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

Коэффициент отказов: Существует сильная отрицательная корреляция между показателем отказов и коэффициентом конверсии. Показатели отказов 0,01 и выше связаны с конверсией ниже среднего.

Время, проведенное на страницах с товарами: Существует сильная положительная корреляция между временем, проведенным посетителями на товарных страницах и коэффициентом конверсии. Люди, которые провели на товарных страницах от 3200 до 6400 единиц времени, имели конверсию ~30 %. Эта информация может помочь в повышении продаж, например, если установить на сайте тайм-трекер и, как только люди превысят порог времени, проведенного на страницах, связанных с товарами, предложить им скидку, чтобы побудить их купить продукт.

Время, проведенное на информационных страницах: Существует слабая положительная корреляция между временем, проведенным посетителями на информационных страницах, и коэффициентом конверсии. Если при 130 единицах времени наблюдается явный скачок, т. е. люди, потратившие более 130 единиц, имеют коэффициент конверсии не менее 25% по сравнению с 15% у тех, кто потратил менее 130 единиц времени, то у тех, кто потратил 520-650 единиц, коэффициент конверсии составляет всего 27%.

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

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

Количество посещенных административных страниц: В целом, здесь корреляция не прослеживается, но те посетители, которые вообще не посещают административные страницы, покупают значительно реже (9%), чем те, кто посещает одну или несколько страниц (20%). Опять же, эта переменная может сильно коррелировать со временем, проведенным на административных страницах.

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

Близость к особому дню: На первый взгляд может показаться интуитивно понятным, что конверсия должна меняться. Тем не менее данные говорят о том, что близость дня посещения веб-сайта к особому дню не приводит к повышению конверсии, даже скорее напротив — к ее снижению. Конечно, это зависит от того, как мы определяем степень близости, но если 0 означает, что день визита далек от специального дня, а 1 — что день визита приходится на специальный день, то здесь явно прослеживается отрицательная корреляция.

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

Выходные: В выходные дни коэффициент конверсии выше, чем в будни: 17% против 15%.

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

Тип трафика: Тип трафика 2 приводит к максимальному количеству конверсий (587) и имеет относительно высокий показатель конвертации в покупки (20%). Команда IT-специалистов может выяснить, почему наибольшее число приносящих доход людей относится именно к этому типу, и если эта переменная поддается изменению, то можно ли конвертировать в трафик этого типа и других людей?

Читайте также:  Алгоритмы программирования. Что важно знать трейдеру и инвестору?

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

Браузер: Большинство покупателей, похоже, используют браузер 2, но тип браузера не имеет особой корреляции с коэффициентом конверсии.

Операционная система: Хотя на операционные системы 2 и 1 приходится большая часть посетителей, приносящих доход, корреляция коэффициентов конверсии с этой переменной также отсутствует.

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

Оптимизация дизайна сайта

Инвестиции в создание более привлекательного и удобного для пользователей веб-сайта могут потенциально привести к росту продаж.

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

Скорость загрузки сайта

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

Конверсия новых и вернувшихся посетителей

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

Сезонность спроса

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

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

Предобработка данных и применение алгоритмов машинного обучения (RandomForest и XGBoost)

Ну, что-ж, с разведкой закончили. Теперь пора переходить к предварительной обработке данных и применении алгоритмов контролируемого обучения, а именно RandomForest и XGBoost.

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

Зачем нужен этап предобработки данных?  «Что посеешь, то и пожнешь». Эта пословица, столь верная для жизни в целом, в значительной степени относится и к Data Science! Мы не можем подавать плохие данные в наши алгоритмы и ожидать, что они волшебным образом дадут нам точные прогнозы. Подготовка данных в форме, которую можно использовать в алгоритме обучения, — важнейшая задача, которую решает специалист по науке о данных.

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

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

  • Выполнение энкодинга (one-hot-encoding) всех категориальных атрибутов;
  • Преобразование непрерывных и дискретных атрибутов из string в тип float;
  • Масштабирование всех числовых (непрерывных и дискретных) атрибутов;
  • Объединение всех атрибутов точек данных в один список с каждым значением в диапазоне 0 и 1;
  • Создание Pandas датафреймов для обучающих и тестовых наборов данных.

Итак, пора приступать к самому интересному — коду. Начнем с библиотек Python, которые понадобятся для предобработки данных и реализации алгоритмов:

#Import Libraries
import numpy as np
from numpy import array
from numpy import argmax

import pandas as pd
from csv import reader

import sklearn
import sklearn.datasets
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
onehot_encoder = OneHotEncoder(sparse=False)
from sklearn.metrics import accuracy_score
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from collections import Counter

import xgboost
from xgboost import XGBClassifier

import imblearn
from imblearn.under_sampling import RandomUnderSampler
undersample = RandomUnderSampler(sampling_strategy=0.5)

# import packages for hyperparameters tuning
from hyperopt import STATUS_OK, Trials, fmin, hp, tpe

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

  • XGBoost;
  • Imblearn;
  • Hyperopt.

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

Вот код, определяющий функции, которые будут использоваться для выполнения следующих стандартных операций над данными:

  • load_csv: Эта функция открывает файл csv и создает список списков, где каждый список — это одна точка данных из набора данных тренировки/теста;
  • str_column_to_float: Эта функция преобразует строковую запись в указанном столбце набора данных в число в формате float.
#Read a CSV file and create a list containing the loaded dataset
def load_csv(filename):
	dataset = list()
	with open(filename, 'r') as file:
		csv_reader = reader(file)
		for row in csv_reader:
			if not row:
				continue
			dataset.append(row)
	return dataset

# Convert string column to float
def str_column_to_float(dataset, column):
	for row in dataset:
		row[column] = float(row[column].strip())

Теперь мы можем загрузить файлы Training и Test Data с помощью функции load_csv.

# load and prepare data
filename = 'input_training_data.csv'
training_data=load_csv(filename)
filename2= 'input_test_data.csv'
test_data=load_csv(filename2)

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

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

#list of attributes that must be one hot encoded
ohe_attributes_list=[['visitor_type',16],['month_train',11],['operating_systems',12],
                     ['browser_train',13],['region_train',14],['traffic_type',15],
                     ['weekend_train',17]
                     ]

#print(ohe_attributes_list[3][1])

for i in range(len(ohe_attributes_list)):
    a_train=[]
    for j in range (1,len(training_data)):
        a_train.append(training_data[j][ohe_attributes_list[i][1]])
    a_train_array = array(a_train)
    a_integer_encoded = label_encoder.fit_transform(a_train_array)
    a_integer_encoded = a_integer_encoded.reshape(len(a_integer_encoded), 1)
    a_onehot_encoded = onehot_encoder.fit_transform(a_integer_encoded)
    a_onehot_encoded_list = a_onehot_encoded.tolist()

    for k in range(1,len(training_data)):
        training_data[k][ohe_attributes_list[i][1]]=a_onehot_encoded_list[k-1]

Следующий шаг — преобразование непрерывных и дискретных атрибутов, представленных в наборе данных в строковой форме (string), в формат с плавающей запятой (float). Иногда эти атрибуты присутствуют в строковой форме в CSV-файле, и их необходимо сначала преобразовать в формат данных с плавающей запятой.

Приведенный ниже сценарий преобразует 10 атрибутов, таких как Exit Rate, Bounce Rate и т. д., и y-значение (статус Revenue, 0 или 1) из формата string в формат float.

#Convert String to Float: All columns that are numerical 
- both discrete and continuous
for i in range(1,11):
    str_column_to_float(training_data[1:], i)
    
for i in range(18,19):
    str_column_to_float(training_data[1:], i)

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

Читайте также:  Определение лучших источников трафика сайта с помощью Python

Хотя деревья решений не так чувствительны к разным диапазонам атрибутов данных, так как к примеру, нейронные сети, многие специалисты по исследованию данных считают лучшей практикой масштабирование входных признаков перед их передачей в алгоритмы. Я применил min-max масштабирование, которое отображает каждый атрибут в значение между 0 и 1.

#Scaling of Columns
for j in range(1,11):
    column=[]
    for i in range (1,len(training_data)):
        column.append(training_data[i][j])
    min_val=min(column)
    max_val=max(column)
    for k in range (1,len(training_data)):
        training_data[k][j]=((training_data[k][j]-min_val)/(max_val-min_val))

Теперь у нас есть набор данных, в котором некоторые признаки представляют собой масштабированные значения в диапазоне от 0 до 1, а другие признаки — это списки 1 и 0, созданные OHE.

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

for i in range(1,len(training_data)):
    a=[]
    for j in range (1,11):
        a.append(training_data[i][j])
    del (training_data[i][1:11])
    training_data[i].insert(1,a)

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

b=[ [] for _ in range(len(training_data)-1) ]
for i in range(1,len(training_data)):
    for j in range(len(training_data[1])):
        if (isinstance(training_data[i][j], list)==0):
            b[i-1].append(training_data[i][j])
        elif (isinstance(training_data[1][j], list)==1):
            for k in range (len(training_data[i][j])):
                b[i-1].append(training_data[i][j][k])

Значения x и y обучающих данных извлекаются и помещаются в отдельные списки:

training_data=b;
y_train=[]
x_train=[]
for i in range(len(training_data)):
    y_train.append(training_data[i][74])
    x_train.append(training_data[i][0:74])

Затем я преобразовал данные x_train и y_train в датафрейм Pandas. Чтобы настроить гиперпараметры алгоритмов обучения, я создал проверочный набор из обучающего набора.

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

temp_list=[]
temp_list.append('ID')
for i in range(1,74):
    j='var'+str(i)
    temp_list.append(j)

df1=pd.DataFrame(x_train,columns=temp_list)

#print(df1.columns)

for col in df1:
    if (col=='ID'):
        pass
    else:
        df1[col] = df1[col].astype(float) 

#print(df1.dtypes)

df2=pd.DataFrame(y_train,columns=['Revenue']) 
#print(df2.dtypes)

X=df1.drop('ID', axis=1)
y=df2.Revenue

X_train, X_validation, y_train, y_validation = train_test_split(X, y, test_size = 0.2, random_state = 0,stratify=y,shuffle=True)
print(Counter(y_train))
print(Counter(y_validation))

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

Построение модели с помощью классификатора RandomForest

RandomForest — это алгоритм контролируемого (supervised) обучения, который объединяет несколько деревьев решений и генерирует взвешенный результат на основе правил принятия решений, собранных из этих деревьев.

Да, по современным меркам, это довольно слабый алгоритм и я знал, что в конечном итоге мне понадобится использовать что-то более мощное, например XGBoost. Тем не менее, я хотел сначала попробовать более простую ML-модель, чтобы установить базовый уровень. Я использовал библиотеку RandomForestClassifier() из scikit-learn и подогнал модель, используя обучающие данные (я не настраивал гиперпараметры для RandomForest, поэтому здесь нет валидного набора).

clf_4 = RandomForestClassifier()
clf_4.fit(X, y)
 
# Predict on training set
pred_y_4 = clf_4.predict(X)

print( np.unique( pred_y_4 ) )

 
# accuracy 
print( accuracy_score(y, pred_y_4) )

 
# auroc
prob_y_4 = clf_4.predict_proba(X)
prob_y_4 = [p[1] for p in prob_y_4]
print( roc_auc_score(y, prob_y_4) )

В результате выполнения кода я заметил, что RandomForest обучился до roc_auc_score 0,89. И это весьма неплохо!

Несмотря на то, что этот показатель хорош, я решил, что этого будет недостаточно, и пора перейти к XGBoost. Да, можно было добиться и большего, но мне не хотелось тратить время на настройку RandomForest, поскольку маловероятно, что настроенный RandomForest даст более высокий показатель roc_auc_score, чем XGBoost с хорошими гиперпараметрами.

Построение модели с помощью классификатора XGBoost

eXtreme Gradient Boosting — это хорошо известный алгоритм машинного обучения, который добавляет все новые деревья решений до тех пор, пока не будет наблюдаться улучшение в задаче классификации / регрессии.

Изначально я использовал настройки XGB по умолчанию, обучил его на обучающем множестве и обнаружил, что его roc-auc_score на тестовом множестве составил впечатляющие 0,916

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

Следующим шагом для меня стала оптимизация гиперпараметров XGBoost, о которой я расскажу ниже.

Оптимизация гиперпараметров для XGBoost

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

Чтобы код не получился громоздким, я определил пространство параметров со словарем, содержащим имена и диапазоны различных гиперпараметров, и целевой функцией для поиска набора параметров, которые максимизируют roc_auc_score (минимизируют 1-roc_auc_score). Затем я провел 1000 испытаний (кажется что много, но на самом деле это крошечная часть исчерпывающего набора параметров ~75 000 000 000 000), чтобы найти (почти) оптимальный набор гиперпараметров. На эту процедуру ушел почти 1 час.

#XGBoost Hyperparameter Optimization
space={ 'max_depth': hp.quniform('max_depth', 5,15,1),
        'learning_rate' : hp.quniform('learning_rate', 0.01, 0.25, 0.01),
        'reg_alpha' : hp.quniform('reg_alpha',1,10,1),
        'reg_lambda' : hp.quniform('reg_lambda',1,10,1),
        'colsample_bytree' :hp.quniform('colsample_bytree',0.1,1,0.1),
        'min_child_weight' : hp.quniform('min_child_weight',0,10,1),
        'subsample' : hp.quniform('subsample',0.1,1,0.1),
        'max_delta_step' : hp.quniform('max_delta_step',1,10,1),
        'n_estimators': hp.quniform('n_estimators',100,300,100),
        'scale_pos_weight':hp.quniform('scale_pos_weight',1,10,1),
        'random_state': 0,
        'gamma':hp.quniform('gamma',1,10,1)
    }

def objective(space):
    clf=XGBClassifier(
                    n_estimators =int(space['n_estimators']), max_depth =int(space['max_depth']), gamma = int(space['gamma']),
                    reg_alpha = int(space['reg_alpha']),min_child_weight=int(space['min_child_weight']),
                    colsample_bytree=space['colsample_bytree'],reg_lambda = int(space['reg_lambda']),
                    subsample = space['subsample'],max_delta_step = int(space['max_delta_step']),
                    learning_rate = space['learning_rate'],scale_pos_weight = int(space['scale_pos_weight']),
                    random_state = space['random_state'])
    
    evaluation = [( X_train, y_train), ( X_validation, y_validation)]
    
    clf.fit(X_train, y_train,
            eval_set=evaluation, eval_metric="auc",
            early_stopping_rounds=10,verbose=False)
    

    pred = clf.predict(X_validation)
    accuracy = accuracy_score(y_validation, pred)
    print ("SCORE:", accuracy)
    prob_y_validation = clf.predict_proba(X_validation)
    prob_y_validation_1 = [p[1] for p in prob_y_validation]
    return {'loss': 1-roc_auc_score(y_validation,prob_y_validation_1), 'status': STATUS_OK }
  
  trials = Trials()

best_hyperparams = fmin(fn = objective,
                        space = space,
                        algo = tpe.suggest,
                        max_evals = 1000,
                        trials = trials)
 

print(best_hyperparams)

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

for col in X:
    if (col=='ID'):
        pass
    else:
        df1[col] = df1[col].astype(float) 

print(X.dtypes)
print(y.dtypes)

df2=pd.DataFrame(y_train,columns=['Revenue']) 
print(df2.dtypes)

#Itertion 5:
clf_5 = XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1,
              colsample_bynode=1, colsample_bytree=1, gamma=9, gpu_id=-1,
              importance_type='gain', interaction_constraints='',
              learning_rate=0.01, max_delta_step=7, max_depth=10,
              min_child_weight=5, monotone_constraints='()',
              n_estimators=200, n_jobs=0, num_parallel_tree=1, random_state=0,
              reg_alpha=1, reg_lambda=10, scale_pos_weight=6, subsample=0.8,
              tree_method='exact', validate_parameters=1, verbosity=None)

clf_5.fit(X, y)
 
# Predict on training set
pred_y_5 = clf_5.predict(X)

prob_y_5 = clf_5.predict_proba(X)

prob_y_5 = [p[1] for p in prob_y_5]
print( roc_auc_score(y, prob_y_5) )

Процедура оптимизации особенно помогла найти правильную комбинацию настроек для learning_rate, max_depth, reg_alpha и reg_lambda.

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

#Prediction on Test Data
#one_hot encoding of visitor_type attribute in test dataset
for i in range(len(ohe_attributes_list)):
    a_test=[]
    for j in range (1,len(test_data)):
        a_test.append(test_data[j][ohe_attributes_list[i][1]])
    a_test_array = array(a_test)
    a_integer_encoded = label_encoder.fit_transform(a_test_array)
    a_integer_encoded = a_integer_encoded.reshape(len(a_integer_encoded), 1)
    a_onehot_encoded = onehot_encoder.fit_transform(a_integer_encoded)
    a_onehot_encoded_list = a_onehot_encoded.tolist()

    for k in range(1,len(test_data)):
        test_data[k][ohe_attributes_list[i][1]]=a_onehot_encoded_list[k-1]

#Convert String to Float: All columns that are numerical - both discrete and continuous

for i in range(1,11):
    str_column_to_float(test_data[1:], i)
    
#Normalization of Columns
    
for j in range(1,11):
    column=[]
    for i in range (1,len(test_data)):
        column.append(test_data[i][j])
    min_val=min(column)
    max_val=max(column)
    for k in range (1,len(test_data)):
        test_data[k][j]=((test_data[k][j]-min_val)/(max_val-min_val))

# Combine non-list attributes of test_data into a list 
        
for i in range(1,len(test_data)):
    a=[]
    for j in range (1,11):
        a.append(test_data[i][j])
    del (test_data[i][1:11])
    test_data[i].insert(1,a)
    
# Combine all lists in a test_data sample into a single list 

b=[ [] for _ in range(len(test_data)-1) ]
for i in range(1,len(test_data)):
    for j in range(len(test_data[1])):
        if (isinstance(test_data[i][j], list)==0):
            b[i-1].append(test_data[i][j])
        elif (isinstance(test_data[1][j], list)==1):
            for k in range (len(test_data[i][j])):
                b[i-1].append(test_data[i][j][k])
            
x_test=b;

df3=pd.DataFrame(x_test,columns=temp_list) 
 
X_test = df3.drop('ID', axis=1)    

# Predict on training set
pred_y_test = clf_5.predict(X_test)

print( np.unique( pred_y_test ) )

prob_y_test = clf_5.predict_proba(X_test)
prob_y_test_final = [p[1] for p in prob_y_test]

import csv
with open('Probability_Prediction_XG_Boost_hp_tuning_14.csv', 'w', newline='') as myfile:
     wr = csv.writer(myfile, quoting=csv.QUOTE_ALL)
     wr.writerow(prob_y_test_final)

Результаты

Используя оптимизированный по гиперпараметрам XGB, я получил впечатляющий результат 0.9318 roc_auc_score! Можно ли было его улучшить? Вероятно, что да. Но тут возникает вопрос целесообразности временных затрат и компьюта. Учитывая, что даже такой результат позволил мне войти в топ-20 этого соревнования kaggle, я подумал что этого достаточно.

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

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