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