Данные предоставлены Maven Analytics
Часть: 2 из 3
Ссылка: https://github.com/DaDataGuy/Telecom-Churn-Analysis
Отток клиентов в сфере телекоммуникаций: https: //www.mavenanalytics.io/data-playground

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

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

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

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

P.S. ссылка на этот набор данных и мой файл Jupyter Notebook указаны вверху. Не стесняйтесь скачивать и следовать.

Моя первая задача — создать новый блокнот Jupyter и начать загрузку библиотек и файла CleanedDF.csv.

import sys
print("Python Version is: " + sys.version)
output: Python Version is: 3.9.7 (default, Sep 16 2021, 16:59:28) [MSC v.1916 64 bit (AMD64)]

Я использую множество библиотек, и если у вас их нет, вы можете либо установить их с помощью anaconda, либо использовать «! pip install xgboost» в блоке ячеек. Если это невозможно, закомментируйте библиотеки, которых у вас нет, и вы сможете внести изменения позже.

import time
import pandas as pd
import itertools
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import joblib #load model
import subprocess
from sklearn import feature_selection
#
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV
#
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression,RidgeClassifier,SGDClassifier
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, BaggingClassifier
from sklearn.preprocessing import OneHotEncoder , OrdinalEncoder,StandardScaler , MinMaxScaler, MaxAbsScaler
from sklearn.naive_bayes import BernoulliNB
from sklearn.ensemble import VotingClassifier
from sklearn.svm import SVC
from sklearn.decomposition import PCA
from sklearn.tree import DecisionTreeClassifier,ExtraTreeClassifier 
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.feature_selection import RFE
from lightgbm import LGBMClassifier
from xgboost import XGBClassifier
from sklearn.metrics import roc_auc_score, recall_score, confusion_matrix, classification_report,f1_score,accuracy_score 
from sklearn.dummy import DummyClassifier
from catboost import CatBoostClassifier
#imblen learn
from imblearn.over_sampling import SMOTE
from imblearn.pipeline import Pipeline as imbpipeline
 
# Get multiple outputs in the same cell
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
 
# Ignore all warnings
import warnings
warnings.filterwarnings('ignore')
warnings.filterwarnings(action='ignore', category=DeprecationWarning)
pd.set_option('display.max_columns', None)
pd.set_option('display.float_format', lambda x: '%.2f' % x) 
# goes to two decimal places 

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

import time
time_begin = time.time()
df = pd.read_csv("CleanedDF.csv") # data = pd.read_csv("census.csv")
print(f'Run time: {round(((time.time()-time_begin)/60), 3)} mins')
Output:
Run time: 0.001 mins

После загрузки всех библиотек и очищенного фрейма данных DF я назначаю фрейм данных, помеченный как df, X, а целевую переменную Churn — y.

X = df
y = df['Churn']

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

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

X_train, X_test, y_train, y_test = train_test_split(X, y, 
                                                    test_size=0.3, 
                                                    random_state=42,
                                                    stratify=y,
                                                    shuffle = True
                                                   )

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

remove = ['CustomerID','ZipCode','City','ChurnCategory','ChurnReason','Churn']
# Numerical columns
num_feats = [ 
 'Age',
 'NumberofDependents',
 'Population',
 'NumberofReferrals',
 'TenureinMonths',
 'AvgMonthlyLongDistanceCharges',
 'AvgMonthlyGBDownload',
 'MonthlyCharge',
 'TotalCharges',
 'TotalRefunds',
 'TotalExtraDataCharges',
 'TotalLongDistanceCharges',
 'TotalRevenue'
]
# Categorical columns
cat_feats = [ 
 'Gender',
 'Offer',
 'Married',
 'PhoneService',
 'MultipleLines',
 'InternetService',
 'InternetService',
 'InternetType',
 'OnlineSecurity',
 'OnlineBackup',
 'DeviceProtectionPlan',
 'PremiumTechSupport',
 'StreamingTV',
 'StreamingMovies',
 'StreamingMusic',
 'UnlimitedData',
 'Contract',
 'PaperlessBilling',
 'PaymentMethod',
]

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

X_testcopy = X_test.copy()
X_test.drop(remove, axis = 1, inplace = True)
X_train.drop(remove, axis = 1, inplace = True)

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

def get_pipeline(X, model):
numeric_pipeline = SimpleImputer(strategy='mean')
    categorical_pipeline = OneHotEncoder(handle_unknown='ignore')
preprocessor = ColumnTransformer(
        transformers=[
            ('numeric', numeric_pipeline, num_feats),
            ('categorical', categorical_pipeline, cat_feats),
            ], remainder='passthrough'
    )
bundled_pipeline = imbpipeline(steps=[
        ('preprocessor', preprocessor),
        ('smote', SMOTE(random_state=42)),
        ('scaler', MinMaxScaler()),
        ('model', model)
    ])
    
    return bundled_pipeline

ПАУЗА

Давайте разберем это шаг за шагом.

  1. Создайте функцию, которая принимает входные данные X и модели.
  2. Внутри функции я использую созданные выше переменные и назначаю их методу преобразования под названием SimpleImputer. Simple Imputer обрабатывает отсутствующие данные, и я указал, что он заменит отсутствующие данные средним значением столбца в наборе данных X_test и X_train.
  3. То же самое относится и к следующему шагу, который использует OneHotEncoder. Один Hot Encoder берет категориальные данные, которые я специально создал, называемые cat_feats, и создает столбцы для каждой уникальной строки. Вы можете узнать больше, нажав на ссылку выше.
  4. Мой препроцессор берет два столбца, которые я создал, и назначает ряд задач по преобразованию данных. Два преобразования — это SimpleImputer, использующий «num_feats», и OneHotEncoder, использующий «cat_feats».
  5. Теперь я создам конвейер, который выполняет преобразования препроцессора, обрабатывает любые несбалансированные данные с помощью SMOTE, масштабирует числовые значения в каждом столбце, чтобы уменьшить влияние выбросов, и применяю его к моей модели.

Следующая часть будет длинной и, честно говоря, стоит потратить время, чтобы узнать, как это работает. Мой общий обзор заключается в том, что эта функция select_model возьмет ваши наборы данных X и Y и пропустит их через конвейер. Функция select_model работает с различными моделями, которые я указал, а затем возвращает точность и время выполнения во фрейме данных. Я заранее извиняюсь за то, как это отформатировано. В конце я предоставлю код Jupyter Notebook, и он будет представлять код в каждом блоке ячеек более понятным образом.

def select_model(X, y, pipeline=None):  
    classifiers = {}
    classifiers.update({"DummyClassifier": DummyClassifier(strategy='most_frequent')})
    classifiers.update({"XGBClassifier": XGBClassifier(use_label_encoder=False, 
                                                       eval_metric='logloss',
                                                       objective='binary:logistic',
                                                      )})
    classifiers.update({"LGBMClassifier": LGBMClassifier()})
    classifiers.update({"RandomForestClassifier": RandomForestClassifier()})
    classifiers.update({"DecisionTreeClassifier": DecisionTreeClassifier()})
    classifiers.update({"ExtraTreeClassifier": ExtraTreeClassifier()})
    #classifiers.update({"ExtraTreesClassifier": ExtraTreeClassifier()})    
    classifiers.update({"AdaBoostClassifier": AdaBoostClassifier()})
    classifiers.update({"KNeighborsClassifier": KNeighborsClassifier()})
    classifiers.update({"RidgeClassifier": RidgeClassifier()})
    classifiers.update({"SGDClassifier": SGDClassifier()})
    classifiers.update({"BaggingClassifier": BaggingClassifier()})
    classifiers.update({"BernoulliNB": BernoulliNB()})
    classifiers.update({"SVC": SVC()})
    classifiers.update({"CatBoostClassifier":CatBoostClassifier(silent=True)})
    
    # Stacking
    models = []
models = []
    models.append(('XGBClassifier', XGBClassifier(use_label_encoder=False, eval_metric='logloss', objective='binary:logistic')))
    models.append(('CatBoostClassifier', CatBoostClassifier(silent=True)))
    models.append(('BaggingClassifier', BaggingClassifier()))
    classifiers.update({"VotingClassifier (XGBClassifier, CatBoostClassifier, BaggingClassifier)": VotingClassifier(models)})
models = []
    models.append(('XGBClassifier', XGBClassifier(use_label_encoder=False, eval_metric='logloss', objective='binary:logistic')))
    models.append(('LGBMClassifier', LGBMClassifier()))
    models.append(('CatBoostClassifier', CatBoostClassifier(silent=True)))
    classifiers.update({"VotingClassifier (XGBClassifier, LGBMClassifier, CatBoostClassifier)": VotingClassifier(models)})
    
    models = []
    models.append(('XGBClassifier', XGBClassifier(use_label_encoder=False, eval_metric='logloss', objective='binary:logistic')))
    models.append(('RandomForestClassifier', RandomForestClassifier()))
    models.append(('DecisionTreeClassifier', DecisionTreeClassifier()))
    classifiers.update({"VotingClassifier (XGBClassifier, RandomForestClassifier, DecisionTreeClassifier)": VotingClassifier(models)})
models = []
    models.append(('XGBClassifier', XGBClassifier(use_label_encoder=False, eval_metric='logloss', objective='binary:logistic')))
    models.append(('AdaBoostClassifier', AdaBoostClassifier()))
    #models.append(('ExtraTreeClassifier', ExtraTreeClassifier()))
    classifiers.update({"VotingClassifier (XGBClassifier, AdaBoostClassifier)": VotingClassifier(models)})
    
    models = []
    models.append(('XGBClassifier', XGBClassifier(use_label_encoder=False, eval_metric='logloss', objective='binary:logistic')))
    #models.append(('ExtraTreesClassifier', ExtraTreesClassifier()))
    classifiers.update({"VotingClassifier (XGBClassifier)": VotingClassifier(models)})    
    
    df_models = pd.DataFrame(columns=['model', 'run_time', 'accuracy'])
for key in classifiers:
        
        start_time = time.time()
pipeline = get_pipeline(X_train, classifiers[key])
        
        cv = cross_val_score(pipeline, X, y, cv=5, scoring='accuracy')
row = {'model': key,
               'run_time': format(round((time.time() - start_time)/60,2)),
               'accuracy': cv.mean(),
        }
df_models = df_models.append(row, ignore_index=True)
        
    df_models = df_models.sort_values(by='accuracy', ascending=False)
    return df_models

Давайте посмотрим, как это было!

models = select_model(X_train, y_train)
models.sort_values(by=['accuracy','run_time'], ascending=False)

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

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

Создание базовой модели для тестирования

Я сейчас собираюсь:

  1. построить XGBClassifier
  2. Используйте функцию get_pipeline для преобразования моего набора данных
  3. Подгонка преобразованного набора данных к X_train и y_train
  4. Возьмите связанный набор данных конвейера и предскажите, используя новый набор данных X_test, чтобы предотвратить переобучение.
basemodel = XGBClassifier(use_label_encoder = False, eval_metric='logloss', objective='binary:logistic')
bundled_pipeline = get_pipeline(X_train, basemodel)
bundled_pipeline.fit(X_train, y_train)
basemodel_y_pred = bundled_pipeline.predict(X_test)

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

print(classification_report(y_test, basemodel_y_pred))
print(confusion_matrix(y_test, basemodel_y_pred))

Матрица путаницы:

[1384] [153]
[ 185] [373]

Отчет о классификации:

Это последний отсчет!

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

time_begin = time.time() #starts timer
# Loan Model
model = XGBClassifier(
    use_label_encoder = False, eval_metric='logloss', objective='binary:logistic', learning_rate = 0.1,
                     n_estimators = 1000, max_depth = 9, min_child_weight = 1, gamma = 0.4, colsample_bytree = 0.8, 
                      subsample = 0.9, reg_alpha = 1, scale_pos_weight = 1)

model = get_pipeline(X_train,model)
model.fit(X_train,y_train)
y_pred = model.predict(X_test)
# predict target probabilities
test_prob = model.predict_proba(X_test)[:,1]
test_pred = np.where(test_prob > 0.45, 1, 0) #sets the probability threshhold and can be tweaked
# test set metrics
roc_auc_score(y_test, test_pred)
recall_score(y_test, test_pred)
confusion_matrix(y_test, test_pred)
print(classification_report(y_test,test_pred))
print(f'Run time: {round(((time.time()-time_begin)/60), 3)} mins')
# adding predictions and their probabilities to the original test Data frame
X_testcopy['predictions'] = test_pred
X_testcopy['pred_probabilities'] = test_prob
# Exporting the predictions to a new CSV labeled high_churn_list
high_churn_list = X_testcopy[X_testcopy.pred_probabilities > 0.0].sort_values(by=['pred_probabilities'], ascending = False                                                                           ).reset_index().drop(columns=['index'],axis=1)

Рассмотрение обновленной классификации и матрицы путаницы.

[1378] [159]
[179] [379]]

Экспорт результатов:

high_churn_list.to_csv('high_churn_list_model.csv', index = False)

Модель готова! Что теперь?

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

Модель предсказала:

  1. 1378 клиентов, которые не ушли и были правильно предсказаны
  2. 179 клиентов, которые ушли, но модель предсказала, что они не ушли
  3. 159 клиентов, которые не ушли, но модель предсказала, что они ушли
  4. 379 клиентов, которые действительно ушли и были правильно предсказаны

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

На этом заканчивается моя модель прогнозирования оттока клиентов с использованием Python и Scikit-Learn. Спасибо, что дошли до конца, и если у вас есть какие-либо комментарии, предложения или вы хотите сказать спасибо, оставьте комментарий ниже.

Хорошего дня! :)