1. Мотивация проекта

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

Чтобы решить эту проблему, мы должны прибегнуть к решениям машинного обучения (ML), которые способны быстро обрабатывать большие данные и давать достаточно точные результаты. Apache Spark — это среда данных, которая учитывает все эти проблемы, поскольку конвейеры данных, развернутые в Spark, используют распределенные системы, такие как HDFS, для обеспечения масштабируемости моделей машинного обучения.

В этом посте мы сосредоточимся на наборе данных о клиентах вымышленного сервиса потоковой передачи музыки под названием Sparkify. Цель проекта — максимально точно прогнозировать отток клиентов. Мы будем использовать небольшое подмножество (128 МБ) набора данных о клиентах (12 ГБ) для построения, обучения и проверки модели. Данные предоставлены Udacity.

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

2. Исследование данных перед оттоком

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

df.printSchema()

# checking number of nulls and empties in the columns
empty = []
nans = []
nulls = []
for c in df.columns:
    empty.append(df.filter(col(c) == '').count())
    nans.append(df.filter(isnan(c)).count())
    nulls.append(df.filter(isnull(c)).count())
print('empty: {}\nnans: {}\nnulls: {}'.format(empty, nans, nulls))

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

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

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

Что касается дальнейшего анализа, мы можем посмотреть краткую статистику, касающуюся общего количества песен, сыгранных пользователями, и периода времени, в течение которого они были подписаны на сервис. Мы видим, что в среднем пользователи играют около 1000 песен, с минимумом и максимумом соответственно 3 и 8000, тогда как среднее время регистрации составляет примерно 80 дней, с минимумом менее одного дня и максимумом 256 дней.

3. Исследование данных после оттока

Прежде всего, мы определяем отток, глядя на выполненное действие. Если пользователь показывает на своей странице значения «Подтверждение отмены», этот пользователь запросил прерывание службы и покинул Sparkify. Это определение оттока в этом проекте.

make_churn = udf(lambda x: 1 if x=="Cancellation Confirmation" else 0, IntegerType())
df = df.withColumn("churn", make_churn(df.page))

В небольшом наборе данных, который мы используем для анализа, используя приведенное выше определение оттока, из 225 различных пользователей 52 являются оттока пользователей и 173 не оттока пользователей.

Давайте посмотрим на некоторые различия между оттоком и не оттоком пользователей.

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

Глядя на гендерные различия на рис. 3.2, пользователи, которые не уходят, почти на 50% состоят из мужчин и на 50% из женщин, но пользователи, отменяющие подписку, в основном мужчины: >60% мужчин и ‹40% женщин.

Анализируя поведение мужчин между двумя группами на рис. 3.3, можно сделать те же выводы, что и на рис. 3.1: мужчины, покидающие службу, как правило, менее вовлечены.

Что касается различий в поведении между оттоком и отсутствием оттока для location и userAgent, небольшие числа в наборе данных не могут точно сказать нам, есть ли какие-либо соответствующие различия (рис. 3.4 и 3.5).

4. Разработка функций

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

Мы не учитываем информацию об userAgent и местоположении, так как эти столбцы не предоставляют много информации для целей проекта.

Характеристики пользователя: пол, уровень, дни_с_регистрации

Действия пользователя: total_songs, total_likes, total_dislikes, обновить, понизить версию , total_seconds, оплата

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

  • upgrade: обновлял ли пользователь когда-либо (логическое значение)
  • downgrade: если пользователь когда-либо переходил на более раннюю версию (логическое значение)
  • total_songs: количество всех действий с пометкой NextSong для каждого пользователя.
  • total_likes: количество всех действий, помеченных как "ThumbsUp" для каждого пользователя.
  • total_dislikes: количество всех действий, помеченных как "ThumbsDown" для каждого пользователя.
  • total_seconds: сумма длин столбцов для каждого пользователя.
  • days_since_registration: количество дней, прошедших с момента регистрации последней записи каждого пользователя в наборе данных.
  • платная: есть ли у пользователя платная подписка.
# define the user functions to calculate the variables written above
is_thumbs_up = udf(lambda x: 1 if x == "Thumbs Up" else 0, IntegerType())
is_thumbs_down = udf(lambda x: 1 if x == "Thumbs Down" else 0, IntegerType())
has_upgraded = udf(lambda x: 1 if x == "Upgrade" else 0, IntegerType())
has_downgraded = udf(lambda x: 1 if x == "Downgrade" else 0, IntegerType())
is_song = udf(lambda x: 1 if x is not None else 0, IntegerType())
has_been_paid = udf(lambda x:1 if x == "paid" else 0, IntegerType())
# calculate the new columns
df_clean = df.withColumn("thumbsUp", is_thumbs_up(df.page))\
    .withColumn("thumbsDown", is_thumbs_down(df.page))\
    .withColumn("upgraded", has_upgraded(df.page))\
    .withColumn("downgraded", has_downgraded(df.page))\
    .withColumn("songCheck", is_song(df.song))\
    .withColumn("paid", has_been_paid(df.level))
numerical_df = df_clean.groupBy("userId").agg(Fsum("songCheck").alias("total_songs"),\                                             Fsum("thumbsUp").alias("total_likes"),\                                              Fsum("thumbsDown").alias("total_dislikes"),\                                              max("upgraded").alias("upgrade"),\                                              max("downgraded").alias("downgrade"),\                                              Fround(Fsum("length"),2).alias("total_seconds"),\                                              max("paid").alias("paid"),\                                              max("churn").alias("churn"))

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

# correlation analysis among the numerical features and churn variable
corr_df = ml_df.select("churn", "days_since_registration", "total_seconds", "total_songs", "total_likes", "total_dislikes", "upgrade", "downgrade", "paid").toPandas()
sns.set(rc={'figure.figsize':(10,10)},font_scale=1)
# plot correlation matrix
sns.heatmap(corr_df.corr(), annot=True)

total_songs и total_seconds совпадают с точки зрения корреляции, поэтому нам нужен только один из двух. Кроме того, total_likes и total_dislikes очень похожи, поэтому мы можем оставить только один. Мы оставим только total_likes, так как он имеет более высокую корреляцию с оттоком.

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

5. Моделирование

Следующий шаг состоит в настройке модели. В наборе данных у нас есть категориальные и числовые переменные, поэтому с ними нужно обращаться по-разному. Мы будем использовать StringIndexer и OneHotEncoder для обработки категориальных переменных, а затем объединять их в вектор вместе с числовыми переменными с помощью VectorAssembler. Таким образом, у нас есть набор данных, готовый для анализа ML в Spark.

def assemble_ml_data(numeric_cols, index_cols, ml_df):
    
    # indexers and econders needed to deal with categorical variables
    indexers = [StringIndexer(inputCol=col_name, outputCol=col_name + "_index", handleInvalid='keep') for col_name in index_cols]
    encoders = [OneHotEncoder(inputCol=indexer.getOutputCol(), 
                          outputCol="{}_ohe".format(indexer.getOutputCol())) for indexer in indexers]
# collect all variables, categorical and numerical and assemble them into a vector column
    assembler = VectorAssembler(inputCols=[encoder.getOutputCol() for encoder in encoders] + numeric_cols, outputCol="vecfeatures")
    p_stages = indexers + encoders + [assembler]
pipeline = Pipeline(stages = p_stages)
    data = pipeline.fit(ml_df).transform(ml_df)
    
    return data

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

def make_prediction(data, transformer=None, paramGrid=None):
    
    # split data into train and test sets and set the pipeline
    train, test = data.randomSplit([0.8, 0.2], seed=42)
    standardscaler = StandardScaler(inputCol="vecfeatures", outputCol="features", withMean=True, withStd=True)
    pipeline = Pipeline(stages=[standardscaler, transformer])
    
    # if a parameter grid is passed, the function gives the possibility to perform cross-validation
    if paramGrid != None:
        crossval = CrossValidator(estimator=pipeline,
                             estimatorParamMaps=paramGrid,
                             evaluator=MulticlassClassificationEvaluator(),
                             numFolds=3)
    
        model = crossval.fit(train.withColumnRenamed("churn", "label"))
    else:
        model = pipeline.fit(train.withColumnRenamed("churn", "label"))
    
    results = model.transform(test)
    
    # output both the model and the dataset with the prediction column
    return model, results

Поскольку это проблема классификации, мы протестируем алгоритм тремя методами: случайный лес (RF), линейная регрессия (LR) и линейный SVC (LSVC). .

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

def evaluation_metrics(results, label_col="label", pred_col="prediction"):
    
    pandas_df = results.select(label_col, pred_col).toPandas()
    print("Accuracy: ", sum(pandas_df[label_col] == pandas_df[pred_col])/pandas_df.shape[0])
    print(classification_report(np.array(pandas_df[label_col]), np.array(pandas_df[pred_col])))

6.Метрики

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

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

Например, в наборе данных с двумя метками, где одна из двух меток присутствует в 90 % случаев, а другая составляет всего 10 % данных, модель классификации может пометить все прогнозы, поскольку класс присутствует в 90 % случаев. времени.

Это приведет к тому, что модель будет иметь точность 90%, что обычно является отличным результатом. Однако, если целью модели является обнаружение обоих классов, она будет бесполезна, поскольку не позволяет нам классифицировать другую метку. Вот почему необходимы другие показатели, такие как точность, отзыв и оценка f1.

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

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

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

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

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

Значения точности и отзыва варьируются от 0 до 1, где 0 — наихудшее значение, а 1 — наилучшее, тогда как значения для оценки f1 варьируются от -∞ до 1, причем последнее является наилучшим возможным значением.

7. Выбор модели

Лучшим преобразователем выбирается тот, кто представляет лучшие значения для точности, отзыва и оценки f1.

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

В задаче двоичной классификации (0 или 1) возможных случаев, которые могут быть сгенерированы предсказанием, четыре:

  • TP, то есть истинные положительные результаты, когда модель предсказывает положительное значение, а фактическое значение положительное.
  • FP, т. е. ложные срабатывания, когда модель предсказывает положительное значение, но фактическое значение отрицательное.
  • TN, т. е. истинные отрицательные значения, когда модель предсказывает отрицательное значение, а фактическое значение отрицательное.
  • FN, то есть ложноотрицательные результаты, когда модель предсказывает отрицательное значение, но фактическое значение положительное.

Учитывая TP, FP, TN и FN, выбранные показатели определяются, как показано ниже.

  • Точность: (TP + TN)/(TP + FP + TN + FN)
  • Точность: TP/(TP + FP)
  • Отзыв: TP/(TP + FN)
  • Оценка F1: (2 * точность * полнота)/(точность + полнота)

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

Лучшей моделью является LR с лучшими значениями всех четырех рассматриваемых показателей. RF также работает хорошо, а LSVC хуже, с очень низкой точностью и оценкой f1.

8. Настройка и уточнение модели

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

Мы используем ParamGridBuilder для создания сетки параметров для настройки модели. Для LR и мы смотрим на regParam и threshold: первый будет оцениваться для значений 0,0, 0,01, 0,05, 0,1, 0,5, 1 , а второй будет оцениваться для значений 0,3, 0,4, 0,5.

Однако перед настройкой мы можем проверить потенциальный дисбаланс классов. В наборе данных 52 из 225 пользователей уходят, поэтому только 23% пользователей показывают значение изменения равным 1. При запуске модели классификации есть возможность, что модель предсказывает все метки. как один из двух классов, и по-прежнему показывает хорошие результаты по точности. По этой причине мы также принимаем во внимание точность, отзыв и оценку f1.

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

Этот столбец веса будет рассчитываться следующим образом: учитывая n_churn_users и n_not_churn_users соответственно как количество ушедших и не ушедших пользователей, мы определим churn_weight. переменная и переменная not_churn_weight.

Две переменные будут рассчитываться следующим образом:

# let's calculate weights for churn and not churn users
churn_weight = n_not_churn_users/(n_churn_users + n_not_churn_users)
not_churn_weight = n_churn_users/(n_churn_users + n_not_churn_users)
label_weight = udf(lambda x: churn_weight if x==1 else not_churn_weight, DoubleType())
data = data.withColumn("weight", label_weight(data.churn))
data.select("churn", "weight").head(5)

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

Снова запустив LR с весом класса и перекрестной проверкой, мы получим следующие результаты:

с regParam и threshold равными соответственно 0,01 и 0,5

9. Другой подход

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

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

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

Новые функции, которые мы будем использовать для прогнозирования ML, будут следующими.

  • total_upgrades: количество обновлений пользователя.
  • total_downgrades: сколько раз пользователь понизил рейтинг.
  • avg_songs: среднее количество всех действий с пометкой NextSong для каждого пользователя за сеанс.
  • avg_thumbsUp: среднее количество всех действий, помеченных как "ThumbsUp", для каждого пользователя за сеанс.
  • avg_thumbsDown: среднее количество всех действий с пометкой "ThumbsDown" для каждого пользователя за сеанс.
  • num_sessions: общее количество сеансов для каждого пользователя.
  • avg_session_duration: средняя продолжительность сеанса для каждого пользователя.
  • avg_session_gap: средний промежуток времени между двумя сеансами для каждого пользователя.
  • days_since_registration: количество дней, прошедших с момента регистрации последней записи каждого пользователя в наборе данных.
  • платная: есть ли у пользователя платная подписка.
  • пол
  • уровень

Эти новые столбцы рассчитываются следующим образом:

is_thumbs_up = udf(lambda x: 1 if x == "Thumbs Up" else 0, IntegerType())
is_thumbs_down = udf(lambda x: 1 if x == "Thumbs Down" else 0, IntegerType())
has_upgraded = udf(lambda x: 1 if x == "Submit Upgrade" else 0, IntegerType())
has_downgraded = udf(lambda x: 1 if x == "Submit Downgrade" else 0, IntegerType())
is_song = udf(lambda x: 1 if x is not None else 0, IntegerType())
has_been_paid = udf(lambda x:1 if x == "paid" else 0, IntegerType())
df_ses = df.withColumn("thumbsUp", is_thumbs_up(df.page))\
    .withColumn("thumbsDown", is_thumbs_down(df.page))\
    .withColumn("upgraded", has_upgraded(df.page))\
    .withColumn("downgraded", has_downgraded(df.page))\
    .withColumn("songCheck", is_song(df.song))\
    .withColumn("paid", has_been_paid(df.level))
df_ses = df_ses.select("userId", "sessionId", "churn", "ts", "rgstr_time", "thumbsUp", "thumbsDown",
              "upgraded", "downgraded", "songCheck", "paid") # no use of song length because of correlation seen before
# the group by is now done both at the user and session level
df_user_ses = df_ses.groupby("userId", "sessionId").agg(max("churn").alias("churn"),\                                          min("ts").alias("ses_start"),\                                          max("ts").alias("ses_end"),\                                          Fsum("thumbsUp").alias("thumbsUp"),\                                          Fsum("thumbsDown").alias("thumbsDown"),\                                          Fsum("upgraded").alias("upgraded"),\                                          Fsum("downgraded").alias("downgraded"),\                                          Fsum("songCheck").alias("num_songs"),\                                          max("paid").alias("paid"))
df_user_ses = df_user_ses.withColumn("sessionDuration_hours", (col("ses_end") - col("ses_start"))/1000.0/3600.0)
# create window function
user_window = Window \
   .partitionBy("userID") \
   .orderBy(asc("ses_start"))
# get the timestamp at the end of the previous session
df_user_ses = df_user_ses.withColumn("prev_ses_end", lag(col("ses_end")).over(user_window))
# calculate session gap as the difference between the start date of the current session and the end date of the previous one
df_user_ses = df_user_ses.withColumn("sessiongap_hours", (col("ses_start") - col("prev_ses_end"))/1000.0/3600.0)
# summarize all the features as average per session
df_user = df_user_ses.groupby("userId").agg(max("churn").alias("churn"),\                               count("sessionId").alias("num_sessions"),\                               Fround(avg("thumbsUp"),2).alias("avg_thumbsUp"),\                               Fround(avg("thumbsDown"),2).alias("avg_thumbsDown"),\                               Fsum("upgraded").alias("total_upgrades"),\                               Fsum("downgraded").alias("total_downgrades"),\                               Fround(avg("num_songs"),2).alias("avg_songs"),\                               Fround(avg("sessionDuration_hours"),2).alias("avg_session_duration"),\                               Fround(avg("sessiongap_hours"),2).alias("avg_session_gap"),\                               max("paid").alias("paid"))

Новый кадр данных выглядит следующим образом:

Корреляционный анализ числовых характеристик показывает, что новая переменная avg_session_gap является хорошим индикатором оттока.

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

Теперь посмотрим на метрики с учетом LR и веса.

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

10. Заключение

  • Для Sparkify около 20% пользователей решают уйти. Пользователи, которые уходят, в основном мужчины. Пользователи, которые уходят, меньше вовлечены в Sparkify.
  • Среди двух принятых подходов тот, который учитывает всю информацию о пользовательских сеансах, такую ​​как средняя продолжительность сеанса и средний промежуток между сеансами, показывает наилучшие результаты.
  • Характеристики, которые являются лучшими предикторами риска оттока, — это общее время, в течение которого пользователь был подписан на услугу, и средний промежуток времени между сеансами.
  • Наилучшим алгоритмом является линейная регрессия со столбцом весов для обработки дисбаланса классов со значениями точность, отзыв, точность. и оценка f1 0,85.

Подводя итог, мы выполнили следующие шаги:

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

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

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

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

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

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

Однако за счет изменения функций и введения весов меток общее решение улучшилось.

Будущие улучшения заключаются в следующем:

  • Прогнозировать можно не только по оттоку, но и попытаться предсказать, когда клиенты решат перейти на даунгрейд, т.е. сменить подписку с платной на бесплатную.
  • Развертывание в Amazon Web Services (AWS), поскольку текущий анализ был проведен на небольшой части (128 МБ) всего набора данных (12 ГБ). Было бы интересно обучить и протестировать модель на полном наборе данных.

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

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

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

Все скрипты и анализ доступны в следующем репозитории github.

11. Ссылки

http://spark.apache.org/docs/latest/api/python/