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

Методология анализа и обработка данных

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

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

# Import libraries/packages
import numpy as np
from numpy.linalg import eig
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = (18,10)
plt.rcParams['figure.max_open_warning'] = False
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import confusion_matrix, roc_curve, auc
from sklearn.ensemble import GradientBoostingClassifier

# Import data
df = pd.read_csv('churn_clean.csv').reset_index(drop=True)

# Review shape and data types
df.info()
RangeIndex: 10000 entries, 0 to 9999
Data columns (total 50 columns):
 #   Column                Non-Null Count  Dtype  
---  ------                --------------  -----  
 0   CaseOrder             10000 non-null  int64  
 1   Customer_id           10000 non-null  object 
 2   Interaction           10000 non-null  object 
 3   UID                   10000 non-null  object 
 4   City                  10000 non-null  object 
 5   State                 10000 non-null  object 
 6   County                10000 non-null  object 
 7   Zip                   10000 non-null  int64  
 8   Lat                   10000 non-null  float64
 9   Lng                   10000 non-null  float64
 10  Population            10000 non-null  int64  
 11  Area                  10000 non-null  object 
 12  TimeZone              10000 non-null  object 
 13  Job                   10000 non-null  object 
 14  Children              10000 non-null  int64  
 15  Age                   10000 non-null  int64  
 16  Income                10000 non-null  float64
 17  Marital               10000 non-null  object 
 18  Gender                10000 non-null  object 
 19  Churn                 10000 non-null  object 
 20  Outage_sec_perweek    10000 non-null  float64
 21  Email                 10000 non-null  int64  
 22  Contacts              10000 non-null  int64  
 23  Yearly_equip_failure  10000 non-null  int64  
 24  Techie                10000 non-null  object 
 25  Contract              10000 non-null  object 
 26  Port_modem            10000 non-null  object 
 27  Tablet                10000 non-null  object 
 28  InternetService       10000 non-null  object 
 29  Phone                 10000 non-null  object 
 30  Multiple              10000 non-null  object 
 31  OnlineSecurity        10000 non-null  object 
 32  OnlineBackup          10000 non-null  object 
 33  DeviceProtection      10000 non-null  object 
 34  TechSupport           10000 non-null  object 
 35  StreamingTV           10000 non-null  object 
 36  StreamingMovies       10000 non-null  object 
 37  PaperlessBilling      10000 non-null  object 
 38  PaymentMethod         10000 non-null  object 
 39  Tenure                10000 non-null  float64
 40  MonthlyCharge         10000 non-null  float64
 41  Bandwidth_GB_Year     10000 non-null  float64
 42  Item1                 10000 non-null  int64  
 43  Item2                 10000 non-null  int64  
 44  Item3                 10000 non-null  int64  
 45  Item4                 10000 non-null  int64  
 46  Item5                 10000 non-null  int64  
 47  Item6                 10000 non-null  int64  
 48  Item7                 10000 non-null  int64  
 49  Item8                 10000 non-null  int64  
dtypes: float64(7), int64(16), object(27)
# Drop granular data
df.drop([
    'CaseOrder', 
    'Customer_id', 
    'Interaction', 
    'UID', 
    'City', 
    'State',
    'County', 
    'Zip', 
    'Lat', 
    'Lng', 
    'TimeZone', 
    'Job'
], axis=1, inplace=True)

# Drop granular data
df.drop([
    'CaseOrder', 
    'Customer_id', 
    'Interaction', 
    'UID', 
    'City', 
    'State',
    'County', 
    'Zip', 
    'Lat', 
    'Lng', 
    'TimeZone', 
    'Job'
], axis=1, inplace=True)

# Rename non-descript variables
df.rename({
    'Item1':'TimelyResponse',
    'Item2':'TimelyFixes',
    'Item3':'TimelyReplacements',
    'Item4':'Reliability',
    'Item5':'Options',
    'Item6':'RespectfulResponse',
    'Item7':'CourteousExchange',
    'Item8':'ActiveListening'
}, axis=1, inplace=True)

# Create a copy of the original data
df1 = df.copy()

# Check for duplicate values
print('Duplicate Values Found:', df.duplicated().sum())

Найдено повторяющихся значений: 0

# Isolate string variables from numeric variables
object_df = pd.DataFrame()
for col in df.columns:
    if df[col].dtype == object:
        object_df[col] = df[col]
        df = df.drop(col, axis=1)

# Scale the data
scaler = StandardScaler()
df = pd.DataFrame(scaler.fit_transform(df), columns=df.columns)

# Detect outliers
df[df.abs() > 3].dropna(how='all')

# Remove observations with absolute zscore over 3
df = df[df.abs() < 3].dropna()

# Evaluate data loss
lost = ((len(object_df) - len(df)) / len(object_df)) * 100
print('Data Lost: {}%\nData Kept: {}%'.format(lost, 100-lost))

Данные потеряны: 9,15%
Данные сохранены: 90,85%

# Drop outlier observations from object data frame
object_df = object_df.loc[df.index]

# Define dictionary for contract variable
contract = {
    'Month-to-month': 0,
    'One year': 1,
    'Two Year': 2
}

# Encode the contract variable and drop it from object df
df['Contract'] = object_df['Contract'].map(contract)
object_df.drop('Contract', axis=1, inplace=True)

# Instantiate the label encoder
le = LabelEncoder()

# Loop through each variable to encode it
for col in object_df.columns:
    if 'Yes' in object_df[col].values:
        df[col] = le.fit_transform(object_df[col])
        object_df.drop(col, axis=1, inplace=True)

# Get dummy variables
object_df = pd.get_dummies(object_df)

# Combine data frames
df[object_df.columns] = object_df

# Save clean data set to csv file
df.to_csv('knn_clean.csv')

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

# Isolate the target variable from the data
X, y = df.drop('Churn', axis=1), df['Churn']

# Split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7)

# Combine target and explanatory variables
X_train['Churn'] = y_train
X_test['Churn'] = y_test

# Save training and testing data sets to csv files
X_train.to_csv('knn_train.csv')
X_test.to_csv('knn_test.csv')

# Drop target variable from train and test sets
X_train.drop('Churn', axis=1, inplace=True)
X_test.drop('Churn', axis=1, inplace=True)

Анализ оттока с использованием моделей машинного обучения

Компания использовала две модели машинного обучения для прогнозирования оттока клиентов: K-Nearest Neighbours (KNN) и Gradient Boosting. Эти модели были выбраны из-за их надежности при обработке как непрерывных, так и категориальных переменных.

Модель KNN была обучена и протестирована с использованием перекрестной проверки с достижением точности 83,64%. Это означает, что модель правильно предсказала, уйдет ли клиент примерно в 83,64% случаев. Эффективность модели была дополнительно подтверждена с использованием кривой рабочих характеристик получателя (ROC), получив площадь под кривой (AUC) 0,8943, что позволяет предположить, что модель может эффективно различать клиентов, которые уходят, и тех, кто нет.

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

# Create a dictionary to store model metrics
k_scores = {}

# Loop through k values 10 times
for k in range(1,11):
    
    # Define the knn classifier model
    knn = KNeighborsClassifier(n_neighbors=k)
    
    # Evaluate the model using cross-validation
    results = cross_val_score(knn, X, y, cv=10, scoring='accuracy')
    
    # Store the average cv score and k
    k_scores[k] = results.mean()

# Select the highest scoring model
k = max(k_scores, key=k_scores.get)

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

# Fit the model using the training set
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train, y_train)

KNeighborsClassifier (n_neighbors = 9)

# Score the final model
score = knn.score(X_test, y_test)

print(f'Accuracy: {round(score*100, 2)}%')

Точность: 84,45%

# Predict churn using the final model
y_pred = knn.predict(X_test)

# Define function to plot confusion matrix
def plot_cm(y_test, y_pred, mod_name):
    ## Get confusion matrix
    cm = confusion_matrix(y_test, y_pred)
    ## Plot confusion matrix
    labels = ['Positive', 'Negative']
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=labels, yticklabels=labels)
    plt.title('Confusion Matrix: {}'.format(mod_name))
    plt.show()

# Plot the confusion matrix for the knn model
plot_cm(y_test, y_pred, 'KNN')

# Get model probabilities
y_pred_prob = knn.predict_proba(X_test)[:,1]

# Calculate false and true positive rates
fpr, tpr, thresholds = roc_curve(y_test, y_pred_prob)

# Calculate the AUC
auc_score = auc(fpr, tpr)
print(f'AUC: {auc_score}')

ППК: 0,8981965528736664

# Plot the ROC curve
plt.plot(fpr, tpr, label=f'AUC = {round(auc_score, 2)}')

# Plot the diagonal line (random classifier)
plt.plot([0, 1], [0, 1], 'k--')

# Fill the area under the curve
plt.fill_between(fpr, tpr, color='blue', alpha=0.05)
plt.xlabel('False Positive Rate (FPR)')
plt.ylabel('True Positive Rate (TPR)')
plt.title('ROC Curve: KNN')
plt.legend()
plt.show()

Кроме того, была использована модель Gradient Boosting, чтобы понять важность различных функций в прогнозировании оттока. Функции с наивысшими значениями важности Gini: Tenure, MonthlyCharge, Contract, StreamingMovies, StreamingTV, InternetFiberOptic, InternetDSL и Bandwidth_GB_Year. Интересно, что ответы на опросы клиентов не попали в первую двадцатку по важности характеристик.

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

# Instantiate the gradient boosting model
gini = GradientBoostingClassifier(
           n_estimators=1000, random_state=10, n_iter_no_change=10
       )

# Fit the model
gini.fit(X_train, y_train)

GradientBoostingClassifier (n_estimators = 1000, n_iter_no_change = 10, random_state = 10)

# Print the stopping n_estimators value
print(f'Best n_estimators Value: {gini.n_estimators_}')

Лучшее значение n_estimators: 149

# Print the model's accuracy score
print(f'Accuracy: {round(gini.score(X, y)*100, 2)}%')

Точность: 91,49%

# Store the Gini importance values
importance = pd.DataFrame(gini.feature_importances_, index=gini.feature_names_in_, columns=['gini'])
importance.sort_values('gini', ascending=False, inplace=True)

Приведенный ниже код был реализован для выявления активных клиентов, подверженных оттоку.

# Create data frame from customers that have not churned
not_churned = df[df.Churn == 0]

# Split target and explanatory variables
X_true, y_true = not_churned.drop('Churn', axis=1), not_churned['Churn']

# Predict what current customers are at highest risk of churn
y_pred = knn.predict(X_true)

# Plot the confusion matrix
plot_cm(y_true, y_pred, 'KNN Predictions')

# Get model probabilities
y_pred_prob = knn.predict_proba(X_true)[:,1]

# Calculate the amount of positive and negative predictions
pos = np.count_nonzero(y_pred!=0)
neg = np.count_nonzero(y_pred==0)
print(f'({pos}, {neg})')

(388, 6278)

# Calculate the false and true, positive and negative rates
fpr = fpr.mean()
tpr = tpr.mean()
fnr = 1 - tpr.mean()
tnr = 1 - fnr

# Calculate the amount of false positives and negatives
fp = int(fpr * neg)
fn = int((1 - tpr) * pos)

# Compare false negative and positive rates
fnr, fpr

(0.45032866178841624, 0.21361127582903802)

# Combine the predicted values with explanatory features
X_true['Churn'] = y_pred

Основные выводы и выводы

Модели смогли определить клиентов с высоким риском оттока. Из сводной статистики клиентов, которые, по прогнозам, должны были уйти, было обнаружено, что средний срок пребывания таких клиентов в компании составлял всего 10,85 месяца, а средняя ежемесячная плата составляла 181,48 доллара. Обычно они подписывались на услуги потоковой передачи и использовали оптоволоконный интернет, потребляя около 1441,61 ГБ пропускной способности в год.

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

# Recall the customers that have been predicted to churn
churn = df1.loc[
    X_true[X_true.Churn == 1
].index].drop('Churn', axis=1).reset_index(drop=True)

# View summary statistics
churn[['Tenure', 'MonthlyCharge', 'Bandwidth_GB_Year']].describe()

# View total customers at risk of churn by feature
for feature in churn[
    ['Contract', 'StreamingTV', 'StreamingMovies', 'InternetService']
]:
    print(churn[feature].value_counts())

Контракт
Из месяца в месяц 213
Один год 123
Два года 52
StreamingTV
Нет 201
Да 187
Потоковое кино
Да 203
Нет 185
Интернет-сервис
Оптоволокно 220< br /> DSL 95
Нет 73

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

Рекомендации

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

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

Файлы данных, блокноты Jupyter, сценарии Python и отчет о проекте можно найти на GitHub вместе с другими проектами, которые я завершил.