Определение проекта

Я всегда стремлюсь изучать новые фреймворки и расширять свои возможности, поэтому, когда я услышал о возможности проекта, использующего Apache Spark и Hadoop, я был уже очень заинтригован. Изучив основы API PySpark Apache Spark, нет лучшего способа продемонстрировать мастерство машинного обучения, чем в контексте больших данных. Этот проект вращается вокруг ключевой бизнес-проблемы, с которой сталкиваются многие фирмы; Как узнать, какие клиенты хотят уйти, и как наш отдел маркетинга может нацелить их на них?

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

См. разработку проекта и исходный код на GitHub

Обзор

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

Я буду использовать Apache Spark 2.4.0, используя PySpark. PySpark - это Python API для Apache Spark. PySpark.ML - это основной пакет машинного обучения PySpark. PySpark.MLLib - еще одна библиотека машинного обучения, но она основана на RDD Apache Spark (устойчивый распределенный набор данных). Я буду запускать программу, используя фреймворк AWS EMR (Elastic Map Reduce) Hadoop, где можно настраивать размеры кластера и вычислительную мощность, оставляя большую часть конфигурации серверной части, хранилища и программного обеспечения платформе AWS.

PySpark.ML обеспечивает дополнительный уровень абстракции поверх этого и позволяет пользователю работать строго с DataFrames. Программирование с помощью RDD более детально (и предоставляет больше функциональных возможностей), но можно использовать обе структуры одновременно. Я сосредоточусь на использовании нового фреймворка Apache Sparks, PySpark.ML.

Библиотека Apache Spark’s SQL предоставляет возможность запрашивать структурированные данные, но по функциональности эквивалентна библиотеке DataFrame. Код, написанный как на Spark + SQL, так и на Spark + DataFrame, проходит через один и тот же оптимизатор в Spark, поэтому разницы в производительности также не должно быть.

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

  • Насколько рекомендуемые песни соответствуют музыкальному вкусу
  • Частота использования
  • Уровень и статус подписки

Схема набора данных приведена ниже:

root
 |-- artist: string
 |-- auth: string
 |-- firstName: string
 |-- gender: string
 |-- itemInSession: long
 |-- lastName: string
 |-- length: double
 |-- level: string
 |-- location: string
 |-- method: string
 |-- page: string
 |-- registration: long
 |-- sessionId: long
 |-- song: string
 |-- status: long
 |-- ts: long
 |-- userAgent: string
 |-- userId: string

Постановка задачи

Конечная цель - успешно определить пользователей, которые уйдут. Для начала, исследовательский анализ данных важен для понимания набора данных и способности критически осмыслить, как можно определять характеристики. Я буду следовать рабочему процессу PySpark Pipeline, основанному на преобразователях (модификация DataFrame) и оценщиках (алгоритм, подобный модели контролируемого обучения). Во-первых, я проведу исследовательский анализ данных, чтобы изучить данные и посмотреть, какие функции могут оказаться полезными. После этого создание набора функций, которые будут заботиться о создании функций, окажется полезным при подборе и оптимизации моделей. Я буду сравнивать производительность различных алгоритмов контролируемого машинного обучения на проверочном наборе, прежде чем оптимизировать гиперпараметры лучших моделей. Разработка будет проводиться на небольшом подмножестве данных, чтобы сократить время вычислений. По опыту, классификаторы случайного леса являются лучшими и менее затратными с точки зрения вычислений, чем машины опорных векторов. Однако распределенные вычисления могут сделать разницу в сложности подгонки незначительной.

Метрики

Важно, чтобы модель могла найти как можно больше пользователей, рассматривающих возможность замены. Это показатель отзыва. Однако наша модель не может быть основана только на воспоминаниях, так как тогда лучше всего будет предположить, что ВСЕ пользователи будут уходить. Несмотря на успешное увеличение показателя отзыва, в бизнес-среде нереально стимулировать каждого пользователя. Точность дает рейтинг правильно идентифицированных пользователей, которые ушли из всех пользователей, которые были отмечены моделью как уходящие. F-Score (среднее гармоническое значение точности и запоминания) объединяет оба этих показателя в одну оценку. Это будет самый важный показатель.

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

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

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

Основные переменные

itemInSession - n-е действие в текущем сеансе пользователя, length - сколько длится песня, level - платный или бесплатный пользователь, registration - когда пользователь зарегистрировался, ts - когда пользователь перешел на определенную страницу, userId - уникальный идентификатор пользователя, sessionId - уникальный идентификатор для сеанса пользователя, page - что делает действие пользователя (описано ниже)

+--------------------+
|                page|
+--------------------+
|               About|
|          Add Friend|
|     Add to Playlist|
|              Cancel|
|Cancellation Conf...|
|           Downgrade|
|               Error|
|                Help|
|                Home|
|              Logout|
|            NextSong|
|         Roll Advert|
|       Save Settings|
|            Settings|
|    Submit Downgrade|
|      Submit Upgrade|
|         Thumbs Down|
|           Thumbs Up|
|             Upgrade|
+--------------------+

Несущественные переменные

artist firstName lastName auth location method song useragent status - Эти переменные мало говорят нам о том, как ведет себя пользователь, а только о том, кто они. Я не верю, что используемый браузер влияет, например, на отток. Тип музыки, которую слушают, тоже не должен. method и status - это информация из HTTP-запросов, которая мало что может рассказать о поведении пользователя.

Графики взаимосвязей переменных - небольшое подмножество

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

Методология

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

Предварительная обработка набора данных фокусируется на создании функций уровня пользователя из набора данных на уровне щелчка. Это также был, вероятно, самый сложный шаг, поскольку он требует критического мышления в отношении данных и элемента, позволяющего поставить себя на место пользователей. После удаления строк, где userId или sessionId является пустой строкой (предположительно, ошибки или просто отслеживание пользователей, которые еще не подписались). Я начинаю систематизировать черты. Я не очищаю никакие другие важные функции, перечисленные в разделе исследовательского анализа данных, даже несмотря на то, что length содержит нулевые значения или минимальное значение 0.78322 seconds. Хотя на первый взгляд кажется неудобным включать такой короткий клип в потоковую службу, это не означает, что это незаконное значение. Кроме того, допустимы значения NULL, поскольку, например, не все действия связаны с названиями песен. Полученная схема была следующей:

root
 |-- userId: string (nullable = true)
 |-- downgraded: long (nullable = true)
 |-- cancelled: long (nullable = true)
 |-- visited_cancel: long (nullable = true)
 |-- visited_downgrade: long (nullable = true)
 |-- dailyHelpVisits: double (nullable = true)
 |-- dailyErrors: double (nullable = true)
 |-- free: integer (nullable = true)
 |-- paid: integer (nullable = true)
 |-- avgThumbsUp: double (nullable = true)
 |-- avgThumbsDown: double (nullable = true)
 |-- numFriends: long (nullable = false)
 |-- avgSongsTillHome: double (nullable = true)
 |-- avgTimeSkipped: double (nullable = true)
 |-- skipRate: double (nullable = true)

Двумя наиболее важными аспектами этапа предварительной обработки данных, необходимыми для создания данных на уровне пользователя, были:

  1. pyspark.sql.functions.udf - определяемая пользователем функция - которую я часто использовал для отслеживания факторов внутри функций.
  2. pyspark.sql.Window - помогает отслеживать информацию по строкам
  • userId был просто извлечен путем нахождения всех уникальных значений
  • downgrade и cancelled подсчитывают, сколько раз пользователь нажимал кнопки Подтверждение перехода на более раннюю версию или Подтверждение отмены соответственно.
  • visited_cancel и visited_downgrade отличаются от описанных выше функций тем, что, когда пользователь посещает страницу отмены, он обычно требует от них подтверждения этого намерения для предотвращения случайного отказа от подписки. Я считаю, что эта функция явно указывает на отток пользователей. Те, кто рассматривает возможность понижения или отмены, скорее всего, недовольны услугой. Сумма посещений этих страниц на пользователя дает представление о том, сколько раз они думали о том, чтобы бросить курить, даже если некоторые из этих посещений были случайными.
  • dailyHelpVisits и dailyErrors также отслеживают количество посещений каждого из соответствующих сайтов. Пользователи, которые получают много ошибок или нуждаются в большой помощи при взаимодействии с сервисом, скорее всего, будут недовольны этим. Разработка этих переменных потребовала суммирования, а затем усреднения посещений по дневным интервалам. Это было сделано с помощью оконных функций и преобразования временных меток Unix в объекты datetime.date.
  • Пользователи free и paid также могут отличаться по своей склонности к оттоку. Разумно предположить, что независимо от уровня, на который подписан пользователь, они вносят равные суммы дохода в бизнес, будь то за счет своих ежемесячных взносов или рекламы, которую они слушают. Поэтому разумно сохранить как платных, так и бесплатных пользователей. Однако вполне вероятно, что пользователи, которые платят, с большей вероятностью откажутся от подписки - возможно, не переходя сначала на бесплатную подписку, - поскольку они фактически платят деньги за услугу.
  • avgThumbsUp & avgThumbsDown - также имена, которые не требуют пояснений. Он был создан с использованием того же процесса агрегирования оконных функций, что и dailyHelpVisits и dailyErrors. Он отслеживает, сколько раз пользователи оценивали песни вверх или вниз. Если пользователи высоко оценивают много песен, то они, вероятно, довольны услугой, независимо от уровня подписки, на которой они находятся. С другой стороны, большое количество голосов против также указывало бы на недовольство пользователя. Важно помнить, что сами по себе эти функции не являются достаточно сильными индикаторами, поскольку пользователи могут по-разному взаимодействовать с сервисом, при этом одни недовольные вообще не будут взаимодействовать, а другие будут иметь много голосов против.
  • numFriends - это функция, созданная в духе avgThumbsUp, описанного выше. Пользователи, у которых есть сильное сообщество с большим количеством друзей, с меньшей вероятностью откажутся от подписки. Подсчитывается путем суммирования общего количества появлений страницы Добавить друга.
  • avgTimeSkipped была последней функцией, которую я реализовал после того, как решил, что мне следует глубже вникнуть в данные, чтобы увидеть, есть ли идеи, которые я мог почерпнуть, которые не были видны сначала. Эта переменная была сложной, поскольку она требовала вычисления разницы между предполагаемой длиной песни и меткой времени следующего HTTP-соединения. Это также учитывает только одновременные песни в одном сеансе. Предполагается, что песни не воспроизводятся, если пользователь переходит на другую страницу. Одна вещь, которая стала очевидной, заключается в том, что даже если пользователь нажимает кнопки «вверх» или «вниз», песня продолжает воспроизводиться. Поэтому эти события удаляются до начала анализа. Общий подход состоит в том, чтобы также использовать оконные функции и pyspark.sql.functions.lag, чтобы иметь возможность находить разницу между текущей и следующей меткой времени и сравнивать ее с длиной песни.

Реализация

Общий процесс реализации выглядит следующим образом:

  1. Загрузить данные из удаленной базы данных
  2. Создайте новый DataFrame на уровне пользователя на основе данных с детализацией по кликам с помощью функции feature_engineering(). Установите для режима сохранения DataFrame значение true с помощью df_scaled.persist(), чтобы сохранить DataFrame в памяти и помочь сократить время вычислений, поскольку тот же DataFrame используется при создании функций.
  3. Масштабируйте данные с помощью функции feature_scaling()
  4. Определите наборы для обучения, проверки и тестирования с соотношением 0,85: 0,075: 0,075. Это делается с помощью randomSplit(), устанавливая постоянное начальное число и затем снова разделяя результат первого разбиения, чтобы получить наборы для проверки и тестирования.
  5. Определите классификатор, fit() классификатор, а затем вызовите transform() при передаче набора проверки, чтобы получить DataFrame с новым столбцом для прогнозов.
  6. Позвоните по телефону custom_evaluation(), чтобы сравнить прогнозы модели с фактическими ярлыками.

Специальная оценка

Объекты BinaryClassificationEvaluator, поставляемые с PySpark 2.4.0, ограничены в предоставляемых метриках. В отличие от мультиклассового эквивалента оценщика, двоичный предоставляет только Область точного вызова под кривой (PR_AUC) или Область характеристик оператора приема под кривой (ROC_AUC) в качестве показателей. Чтобы получить более глубокую оценку, я оцениваю количество истинных положительных результатов (TP), ложных положительных результатов (FP), истинных отрицательных результатов (TN ) и ложноотрицательных результатов (FN) вручную и распечатайте PR_AUC для сравнения. В некоторых примерах я видел использование мультиклассового оценщика для двоичной оценки, которое затем позволяет легко оценивать различные метрики, однако это не сработало для меня. Более того, BinaryClassificationEvaluator вернул PR_AUC=1 для всех классификаторов. Мне не удалось выяснить это несоответствие в метрике.

Модельное обучение и оценка

Я выбрал четыре алгоритма классификации для первоначального обучения и сравнения показателей на проверочном наборе. Они были выбраны из-за их классификационной и вычислительной эффективности (многослойный классификатор персептронов не учитывался). Четыре алгоритма были:

  1. Случайный лес
  2. Деревья с градиентным усилением
  3. Машины опорных векторов
  4. Логистическая регрессия

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

Уточнение

Поскольку модель показала идеальный классификационный балл для набора проверки, улучшать особо нечего. Однако я хотел выяснить, можно ли использовать анализ основных компонентов для уменьшения количества функций, а также пролить свет на то, какие факторы являются наиболее важными при определении того, захочет ли пользователь отменить. Анализ PCA показывает, что 97,69% дисперсии в наборе данных можно объяснить первыми 6 компонентами. Следовательно, при подборе и прогнозировании было бы разумно сэкономить время и вычислительную мощность, выполнив PCA(k=6)

Полученные результаты

Оценка модели

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

Классификатор случайного леса в основном представлял собой готовый случайный лес PySpark. Ниже приведены основные параметры модели:

featureSubsetStrategy : 'auto' - the number of features to consider for splits at each tree node
numTrees: 10 - number of trees to train & query (default=20)
minInfoGain: 0.0 - min info gain for a split to be considered at a tree node
minInstancesPerNode: 1 - min # of instances each child must have after split
impurity: 'gini' - criterion used for information gain calculation
maxBins: 32 - max # of bins for discretizing continuous features
maxDepth: 5 - maximum depth of the tree
subsamplingRate: 1.0 - fraction of the training data used for learning each decision tree

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

Обоснование

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

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

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

Заключение

Отражение

Классификация вероятности оттока пользователей на основе данных уровня взаимодействия является сложной задачей, поскольку требует высокого уровня проектирования функций, чтобы иметь возможность предсказать, что чувствует пользователь - его удовлетворенность услугой. Естественно, существуют факторы, помимо удовлетворенности пользователей, которые могут повлиять на статус их подписки, которые не включены в предоставленные данные. Однако, несмотря на то, что задача велика, забавный и захватывающий аспект заключается в том, чтобы представить себя пользователям и подумать о причинах, по которым они могут захотеть уйти, а затем создать числовые переменные, которые описали бы это для машины. Распределенные и параллельные вычисления с использованием такой инфраструктуры, как Apache Spark, упрощают этот процесс, позволяя вычислять большие наборы данных в разумные сроки. Спроектированные функции сосредоточены на разнообразных действиях, таких как взаимодействие с сервисом через большой палец вверх, палец вниз или добавление друзей, а также на привычки слушания, которые сосредоточены на времени и привычках, а не на музыкальном вкусе. Эти функции также включают в себя оценку удовлетворенности службой путем отслеживания посещений для помощи или перехода на более раннюю версию и отмены сайтов, или количества ошибок, которые пользователи видят ежедневно. Затем функции масштабируются до диапазона [0,1], чтобы двоичные переменные не искажались, а большие значения не искажали модель. Наконец, анализ главных компонентов используется для уменьшения размерности набора данных и сохранения только 6 основных функций, которые также объясняют более 97% дисперсии в наборе данных.

Часть проекта, которая была сложной, заключалась в расширении масштабов реализации скриптов и алгоритмов с использованием Apache Spark и AWS. Я отслеживал множество ошибок и исключений, которые могли быть связаны только с серверной частью AWS или Hadoop. Кроме того, набор данных настолько велик, с таким количеством возможных функций и объяснений, что также было трудно сузить круг вопросов, на которых следует сосредоточить внимание при разработке функций.

Улучшение

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

Источники