Прогнозирование оттока клиентов со scikit-learn

Показатель оттока клиентов – бизнес-термин, описывающий насколько интенсивно клиенты покидают компанию или прекращают оплачивать товары или услуги. Это ключевой показатель для многих компаний, потому что зачастую приобретение новых клиентов обходится намного дороже, чем удержание старых (в некоторых случаях от 5 до 20 раз дороже).

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

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

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

В этой статье мы рассмотрим моделирование оттока клиентов с помощью Python.

Постойте, не уходите!

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

Собрать команду таких специалистов – задача не из легких. Они должны работать слажено и уметь реагировать на широкий спектр жалоб. Главное здесь – точный таргетинг на основании возможной причины ухода. Мероприятия по удержанию должны «бить прямо в цель» и соответствовать ожидаемой прибыли, которую принесет клиент. Согласитесь, потратить $1 000 на удержание клиента, не собиравшегося уходить, было бы нерационально.

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

Джон Форман (John Forman) из компании MailChimp написал по этому поводу следующее:

«Я работаю в индустрии email-маркетинга в компании MailChimp.com. Мы помогаем компаниям рассылать информационные письма их клиентам, и каждый раз, когда кто-то использует термин «взрывная массовая рассылка» («e-mail blast»), маленькая частичка меня умирает.

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

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

Мы рассмотрим простой пример – как использовать библиотеки Python  для предсказания оттока клиентов и применить данное решение для оптимизации работы команды специалистов по их удержанию.

Набор данных

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

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

In ⌈2⌉:

from __future__ import division
import pandas as pd
import numpy as np

churn_df = pd.read_csv('churn.csv')
col_names = churn_df.columns.tolist()

print "Column names:"
print col_names

to_show = col_names[:6] + col_names[-6:]

print "\nSample data:"
churn_df[to_show].head(6)
Column names:
          ['State', 'Account Length', 'Area Code', 'Phone', "Int'l Plan", 'VMail Plan', 'VMail Message', 'Day Mins', 'Day Calls', 'Day Charge', 'Eve Mins', 'Eve Calls', 'Eve Charge', 'Night Mins', 'Night Calls', 'Night Charge', 'Intl Mins', 'Intl Calls', 'Intl Charge', 'CustServ Calls', 'Churn?']

          Sample data:

Out ⌈2⌉:

3

В этой статье мы будем использовать достаточно простую статистическую модель, поэтому пространство признаков почти полностью соответствует тому, что вы видите выше. Код, представленный ниже, просто исключает не интересующие нас столбцы и преобразует строковые значения в булевы значения (поскольку модели не очень хорошо работают со строковыми значениями «yes» и «no»). Остальные числовые значения остаются без изменений.

In ⌈3⌉:

# Isolate target data
churn_result = churn_df['Churn?']
y = np.where(churn_result == 'True.',1,0)

# We don't need these columns
to_drop = ['State','Area Code','Phone','Churn?']
churn_feat_space = churn_df.drop(to_drop,axis=1)

# 'yes'/'no' has to be converted to boolean values
# NumPy converts these from boolean to 1. and 0. later
yes_no_cols = ["Int'l Plan","VMail Plan"]
churn_feat_space[yes_no_cols] = churn_feat_space[yes_no_cols] == 'yes'

# Pull out features for future use
features = churn_feat_space.columns

X = churn_feat_space.as_matrix().astype(np.float)

# This is important
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)

print "Feature space holds %d observations and %d features" % X.shape
print "Unique target labels:", np.unique(y)

Feature space holds 3333 observations and 17 features
          Unique target labels: [0 1]

Одно небольшое замечание. Для многих предикторов важна относительная величина признаков, даже если их масштабы различны. Например, количество очков, которое баскетбольная команда набирает за игру, естественно, будет на несколько порядков больше, чем отношение количества выигранных матчей к их общему числу. Но это не означает, что второй признак в 100 раз менее значителен. Объект StandardScaler позволяет решить эту проблему при помощи нормализации, приводя значение каждого признака к диапазону от 1,0 до -1,0, предотвращая тем самым неправильное поведение моделей.

Отлично, теперь у нас есть пространство признаков «X» и набор целевых значений «y». Вперед, к прогнозам!

Насколько хороша ваша модель?

Выразить, протестировать, повторить. Процесс реализации машинного обучения может быть каким угодно, но только не статичным. Всегда есть возможность создать новые признаки, использовать новые данные, применить новые классификаторы, у каждого из которых есть специфические параметры, доступные для настройки. И после каждого изменения крайне важно иметь возможность получить ответ на вопрос: «Действительно ли новая версия лучше предыдущей?» Каким же образом это можно реализовать?

В данной статье мы будем использовать перекрестную проверку (cross validation). Перекрестная проверка позволяет избежать переобучения (обучение и тестирование прогнозов на одних и тех же данных), обеспечивая при этом эффективное прогнозирование для каждого набора данных. Это реализуется путем систематического сокрытия различных подмножеств обучающего набора данных во время обучения моделей. После завершения обучения каждая модель тестируется на том подмножестве данных, которое было скрыто от нее при обучении. Таким образом имитируются различные наборы данных для обучения и тестирования. Если все сделано правильно, прогнозы будут «объективными».

Вот как это реализуется с помощью библиотеки scikit-learn.

In ⌈4⌉:

from sklearn.cross_validation import KFold

def run_cv(X,y,clf_class,**kwargs):
    # Construct a kfolds object
    kf = KFold(len(y),n_folds=5,shuffle=True)
    y_pred = y.copy()
    
    # Iterate through folds
    for train_index, test_index in kf:
        X_train, X_test = X[train_index], X[test_index]
        y_train = y[train_index]
        # Initialize a classifier with key word arguments
        clf = clf_class(**kwargs)
        clf.fit(X_train,y_train)
        y_pred[test_index] = clf.predict(X_test)
    return y_pred

Мы решили сравнить три различных алгоритма: метод опорных векторов (support vector machine, SVM), случайный лес (random forest) и метод k ближайших соседей (k-nearest neighbors, KNN). Ничего особенного здесь нет, просто подвергаем каждый алгоритм перекрестной проверке и определяем, как часто классификатор предсказывает правильный класс.

In ⌈5⌉:

from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier as RF
from sklearn.neighbors import KNeighborsClassifier as KNN

def accuracy(y_true,y_pred):
    # NumPy interprets True and False as 1. and 0.
    return np.mean(y_true == y_pred)

print "Support vector machines:"
print "%.3f" % accuracy(y, run_cv(X,y,SVC))
print "Random forest:"
print "%.3f" % accuracy(y, run_cv(X,y,RF))
print "K-nearest-neighbors:"
print "%.3f" % accuracy(y, run_cv(X,y,KNN))
Support vector machines:
          0.918
          Random forest:
          0.943
          K-nearest-neighbors:
          0.896

Случайный лес победил, не так ли?

Точность (precision) и полнота (recall)

Метрики не являются идеальными формулами, которые всегда выставляют высокие оценки хорошим моделям и низкие оценки плохим моделям. В сущности, они выражают некоторое «мнение» относительно эффективности модели, а человек уже самостоятельно должен делать выводы об адекватности оценки. Проблема с метрикой «правильность» (accuracy) заключается в том, что результаты не обязательно эквивалентны. Если классификатор спрогнозирует уход клиента, но клиент останется, – это не очень хорошо, но простительно. Однако если классификатор предскажет, что клиент не уйдет, я не стану предпринимать никакие действия, а клиент все же уйдет – это уже действительно плохо.

Мы используем еще одну встроенную в scikit-learn функцию для создания матрицы ошибок (confusion matrix). Матрица ошибок – это способ визуализации прогнозов классификатора, представляющий собой таблицу, демонстрирующую распределение прогнозов для данного класса. Ось X представляет истинные классы (ушел клиент или нет), а ось Y представляет классы, предсказанные моделью (прогнозы моего классификатора относительно возможного ухода клиента).

In ⌈6⌉:

from sklearn.metrics import confusion_matrix

y = np.array(y)
class_names = np.unique(y)

confusion_matrices = [
    ( "Support Vector Machines", confusion_matrix(y,run_cv(X,y,SVC)) ),
    ( "Random Forest", confusion_matrix(y,run_cv(X,y,RF)) ),
    ( "K-Nearest-Neighbors", confusion_matrix(y,run_cv(X,y,KNN)) ),
]

# Pyplot code not included to reduce clutter
from churn_display import draw_confusion_matrices
%matplotlib inline

draw_confusion_matrices(confusion_matrices,class_names)


charts

Важным вопросом является следующий: «Каково отношение количества правильно спрогнозированных уходов к общему количеству фактических уходов?» Эта метрика имеет название «полнота» (recall). По диаграммам видно, что случайный лес превосходит остальные алгоритмы по данному показателю. Притом, что было 483 фактических случая ухода клиентов (значение «1»), 330 прогнозов оказались правильными. Следовательно, «полнота» для случайного леса составляет примерно 68% (330/483 ≈ 2/3), что значительно лучше, чем результат метода опорных векторов (≈ 50%) или метода k ближайших соседей (≈ 35%).

Другой важный вопрос описывает метрику под названием «точность» (precision): «Каково отношение количества правильно спрогнозированных уходов к общему количеству спрогнозированных уходов?» Случайный лес снова превосходит остальные алгоритмы и демонстрирует «точность» около 93% (330 из 356). Метод опорных векторов немного позади – 87% (235 из 269). Метод k ближайших соседей на последнем месте – около 80%.

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

Мыслить категориями вероятностей

В области принятия решений часто предпочтительнее опираться на вероятность, чем на обычную классификацию. Действительно, в таких высказываниях, как «Вероятность дождя завтра – 20%» или «Около 55% кандидатов успешно сдают экзамен калифорнийской ассоциации адвокатов» содержится намного больше информации, чем в таких высказываниях, как «Завтра не будет дождя» или «Вероятно, вы сдадите экзамен». Предсказание вероятности ухода также позволяет нам оценить ожидаемую прибыль от клиентов и потери от их ухода. К какому из клиентов следует обратиться первым делом, к тому, который платит $20 000 в год и имеет риск ухода 80%, или к тому, который платит $100 000 в год и имеет риск ухода 40%? Сколько средств необходимо потратить на каждого из них?

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

In ⌈7⌉:

def run_prob_cv(X, y, clf_class, **kwargs):
    kf = KFold(len(y), n_folds=5, shuffle=True)
    y_prob = np.zeros((len(y),2))
    for train_index, test_index in kf:
        X_train, X_test = X[train_index], X[test_index]
        y_train = y[train_index]
        clf = clf_class(**kwargs)
        clf.fit(X_train,y_train)
        # Predict probabilities, not classes
        y_prob[test_index] = clf.predict_proba(X_test)
    return y_prob

Насколько хорошо хорошее?

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

В этом случае помогает то, что предикторы делают не один прогноз, а более 3 000 прогнозов. Таким образом, каждый раз, когда мы предсказываем, что событие произойдет в 20% случаев, мы можем узнать, как часто это событие происходит на самом деле. В следующем фрагменте кода библиотека Pandas помогает мне сравнить прогнозы, сделанные с помощью случайного леса, с фактическими исходами.

In ⌈8⌉:

import warnings
warnings.filterwarnings('ignore')

# Use 10 estimators so predictions are all multiples of 0.1
pred_prob = run_prob_cv(X, y, RF, n_estimators=10)
pred_churn = pred_prob[:,1]
is_churn = y == 1

# Number of times a predicted probability is assigned to an observation
counts = pd.value_counts(pred_churn)

# calculate true probabilities
true_prob = {}
for prob in counts.index:
    true_prob[prob] = np.mean(is_churn[pred_churn == prob])
    true_prob = pd.Series(true_prob)

# pandas-fu
counts = pd.concat([counts,true_prob], axis=1).reset_index()
counts.columns = ['pred_prob', 'count', 'true_prob']
counts

Out ⌈8⌉:

4

Случайный лес предсказал, что 75 клиентов имеют вероятность ухода, равную 0,9, и в реальности эта группа имеет показатель ~0,97.

Калибровка (calibration) и дискриминация (discrimination)

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

In ⌈9⌉:

from ggplot import *
%matplotlib inline

baseline = np.mean(is_churn)
ggplot(counts,aes(x='pred_prob',y='true_prob',size='count')) + \
    geom_point(color='blue') + \
    stat_function(fun = lambda x: x, color='red') + \
    stat_function(fun = lambda x: baseline, color='green') + \
    xlim(-0.05,  1.05) + ylim(-0.05,1.05) + \
    ggtitle("Random Forest") + \
    xlab("Predicted probability") + ylab("Relative frequency of outcome")
ggplot

Вы, наверное, обратили внимание на две прямые, начерченные с помощью функции stat_function().

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

Смысл калибровки можно выразить следующим образом: «События, предсказанная вероятность которых составляет 60%, должны происходить в 60% случаев». Для всех клиентов, для которых я прогнозирую риск ухода в диапазоне от 30% до 40%, фактический показатель ухода должен составлять около 35%. В графическом выражении это дает возможность оценить, насколько прогнозы близки к красной линии.

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

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

В scikit-learn данные метрики не реализованы, поэтому мне пришлось сделать это самостоятельно. Ради всеобщего блага я не привожу в этой статье математику и код. Уравнения взяты из публикации Yaniv, Yates, Smith «Measures of Discrimination Skill in Probabilistic Judgment» (1991). А код модуля churn_measurements, из которого импортируются функции ниже, вы можете найти на GitHub здесь.

In ⌈10⌉:

from churn_measurements import calibration, discrimination

Давайте сравним модели на основе этих метрик.

In ⌈11⌉:

def print_measurements(pred_prob):
    churn_prob, is_churn = pred_prob[:,1], y == 1
    print "  %-20s %.4f" % ("Calibration Error", calibration(churn_prob, is_churn))
    print "  %-20s %.4f" % ("Discrimination", discrimination(churn_prob,is_churn))

    print "Note -- Lower calibration is better, higher discrimination is better"

    print "Support vector machines:"
    print_measurements(run_prob_cv(X,y,SVC,probability=True))

    print "Random forests:"
    print_measurements(run_prob_cv(X,y,RF,n_estimators=18))

    print "K-nearest-neighbors:"

print_measurements(run_prob_cv(X,y,KNN))
 Note -- Lower calibration is better, higher discrimination is better
          Support vector machines:
          Calibration Error    0.0017
          Discrimination       0.0667
          Random forests:
          Calibration Error    0.0079
          Discrimination       0.0830
          K-nearest-neighbors:
          Calibration Error    0.0022
          Discrimination       0.0449

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

Практическое применение модели на платформе Yhat

Пришло время загрузить модель в облако! Чтобы продемонстрировать продвинутую функциональность, создадим тестовый набор данных из исходных данных при помощи функции test_train_split() библиотеки scikit-learn. Далее адаптируем классификатор на основе метода опорных векторов для данного случая.

In ⌈12⌉:

from sklearn.cross_validation import train_test_split

train_index,test_index = train_test_split(churn_df.index)
clf = SVC(probability=True)
clf.fit(X[train_index],y[train_index])

test_churn_df = churn_df.ix[test_index]
test_churn_df.to_csv("test_churn.csv")

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

Также мы добавили несколько расчетов. Во-первых, показатель «ценность клиента» (customer worth) (сумма всех платежей, взимаемых с данного клиента). В результате комбинации этой величины с вероятностью ухода клиента получается очень важный показатель: ожидаемая утраченная прибыль в случае ухода данного клиента. Здесь важную роль играет модель, дающая точные прогнозы, поскольку данные значения невозможно получить при помощи одной только классификации.

In ⌈13⌉:

from yhat import Yhat,YhatModel,preprocess

class ChurnModel(YhatModel):
    # Type casts incoming data as a dataframe
    @preprocess(in_type=pd.DataFrame,out_type=pd.DataFrame)
    def execute(self,data):
        # Collect customer meta data
        response = data[['Area Code','Phone']]
        charges = ['Day Charge','Eve Charge','Night Charge','Intl Charge']
        response['customer_worth'] = data[charges].sum(axis=1)
        # Convert yes no columns to bool
        data[yes_no_cols] = data[yes_no_cols] == 'yes'
        # Create feature space
        X = data[features].as_matrix().astype(float)
        X = scaler.transform(X)
        # Make prediction
        churn_prob = clf.predict_proba(X)
        response['churn_prob'] = churn_prob[:,1]
        # Calculate expected loss by churn
        response['expected_loss'] = response['churn_prob'] * response['customer_worth']
        response = response.sort('expected_loss', ascending=False)
        # Return response DataFrame
        return response

yh = Yhat(
    "e[at]yhathq.com",
    "MY API KEY",
    "http://cloud.yhathq.com/"
)

response = yh.deploy("PythonChurnModel",ChurnModel,globals())
Are you sure you want to deploy? (y/N): y

Пакетный режим Yhat

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

В компании Yhat наш принцип – это поиск способов сделать data science применимой и практичной, как можно быстрее. Поскольку функция execute() принимает и возвращает тип данных DataFrame, платформа Yhat позволяет нам использовать режим пакетной оценки (batch-scoring mode). Концепция проста. Пользователь загружает csv-файл из любого места, модель выполняется, и пользователь получает на выходе csv-файл с результатами. Это означает, что данный метод оценки клиентов по риску ухода может использовать любой сотрудник моей компании, независимо от того, какими техническими навыками он обладает, насколько хорошо разбирается в машинном обучении и какой язык программирования использует, Python или R.

После входа на cloud.yhathq.com мы выбираем свою модель, и рабочее пространство принимает вид, изображенный на рисунке ниже. Загруженный csv-файл представлял собой обучающий набор данных, созданный в предыдущем разделе. На практике, это будет новый файл, экспортируемый из нашей CRM-системы, или клиентская база данных, представленная в таком же формате, как и набор данных, который мы использовали для обучения модели.

cloud

Ждем несколько секунд, когда файл с результатами готов, загружаем его по ссылке в нижней части страницы и открываем в Excel.

workbook

Готово. Более 800 клиентов проанализированы и ранжированы в несколько кликов.

По материалам: yhathq.com

1 комментарий

  1. Екатерина:

    код Python в статье отображается примерно вот так:
    class ChurnModel(YhatModel):
    # Type casts incoming data as a dataframe
    @preprocess(in_type=pd.DataFrame,out_type=pd.DataFrame<span

Добавить комментарий

Ваш e-mail не будет опубликован.

закрыть

Поделиться

Отправить на почту
закрыть

Вход

закрыть

Регистрация

+ =