ВВЕДЕНИЕ
Часть 1 читайте здесь: https://medium.com/@alidu143/unveiling-the-drivers-of-customer-churn-an-analytical-journey-to-improve-retention-part-1-20fbfd602841
В части 1 я сосредоточился на данных и их истории, построении всестороннего исследовательского анализа данных, гипотезы и некоторых деловых вопросов (на самом деле вопросов было много). В этом разделе я буду выполнять разработку некоторых функций, а затем создавать модели машинного обучения, чтобы предсказать возможность оттока клиентов.
Обработка функций и разработка
Удалить дубликаты
# Check for duplicate rows in churn_data duplicate_rows = Telco_churn.duplicated() print("Number of duplicate rows:", duplicate_rows.sum()) Number of duplicate rows: 0
Группировка набора данных (функции (X) и целевая переменная (y))
# Split the columns into numerical and categorical data df = Telco_churn.copy() df.drop(['customerID'], axis=1, inplace=True) # split the data into features (X) and target variable (y) X = df.drop('Churn', axis=1) y = df['Churn']
Вмените пропущенные значения
from sklearn.impute import SimpleImputer # Create an instance of the SimpleImputer class with mean strategy imputer = SimpleImputer(strategy='mean') # Impute missing values in TotalCharges column with the mean value of that column X['TotalCharges'] = imputer.fit_transform(X[['TotalCharges']])
Создание новых функций
Основываясь на информации, предоставленной в наборе данных, есть некоторые атрибуты, которые потенциально могут быть объединены для создания новых функций:
«OnlineSecurity» и «DeviceProtection»:
Эти два атрибута указывают, есть ли у клиента определенные виды услуг защиты. Объединение их в одну функцию, такую как «SecurityServices», может упростить набор данных и потенциально повысить точность модели.
# Combine "OnlineSecurity" and "DeviceProtection" into a new feature called "SecurityServices" X['SecurityServices'] = X['OnlineSecurity'] + X['DeviceProtection'] # Replace values with new labels based on whether the customer has no, one, or both security services X['SecurityServices'] = X['SecurityServices'].replace({'NoNo': 'NoneSecurityServices', 'YesNo': 'OnlyOnlineSecurity', 'NoYes': 'OnlyDeviceProtection', 'YesYes': 'BothSecurityServices'}) # Drop the original "OnlineSecurity" and "DeviceProtection" features X = X.drop(['OnlineSecurity', 'DeviceProtection'], axis=1) X['SecurityServices'].head()
"StreamingTV" и "StreamingMovies":
Оба эти атрибута относятся к тому, использует ли клиент потоковые сервисы. Объединение их в одну функцию, например «StreamingServices», также может помочь упростить набор данных.
# Create a new feature called "StreamingServices" X['StreamingServices'] = X['StreamingTV'] + X['StreamingMovies'] # Replace values with new labels based on whether the customer has no, one, or both Streaming Services X['StreamingServices'] = X['StreamingServices'].replace({'NoNo': 'NoneStreamingServices', 'YesNo': 'OnlyStreamingTV', 'NoYes': 'OnlyStreamingMovies', 'YesYes': 'BothStreamingServices'}) # Drop the original "StreamingTV" and "StreamingMovies" features X = X.drop(['StreamingTV', 'StreamingMovies'], axis=1) X['StreamingServices'].head()
"PhoneService" и "MultipleLines":
Оба этих атрибута относятся к телефонной службе клиента. Объединение их в одну функцию, например «PhoneServices», может помочь уменьшить количество функций в наборе данных.
# Combine PhoneService and MultipleLines into a single feature, PhoneServices X['PhoneServices'] = X.apply(lambda x: 'MultipleLines' if x['MultipleLines'] == 'Yes' else 'SingleLine', axis=1) # Drop PhoneService and MultipleLines columns X.drop(['PhoneService', 'MultipleLines'], axis=1, inplace=True) X['PhoneServices'].head(100)
"InternetService" и "OnlineBackup":
Оба этих атрибута относятся к интернет-сервису клиента. Объединение их в одну функцию, например «InternetServices», может помочь упростить набор данных.
# Create a new feature called "InternetServices" X['InternetServices'] = X.apply(lambda row: 'DSL Only' if row['InternetService'] == 'DSL' and row['OnlineBackup'] == 'No' else 'Fiber Optic Only' if row['InternetService'] == 'Fiber optic' and row['OnlineBackup'] == 'No' else 'Internet and Backup' if (row['InternetService'] == 'DSL' or row['InternetService'] == 'Fiber optic') and row['OnlineBackup'] == 'Yes' else 'No Internet Service', axis=1) # Drop the original "InternetService" and "OnlineBackup" columns X = X.drop(['InternetService', 'OnlineBackup'], axis=1) X['InternetServices'].head()
Особенности кодирования
# Import the necessary libraries from sklearn.preprocessing import OneHotEncoder columns_to_encode = ['Partner', 'Dependents', 'PaperlessBilling', 'gender'] # Create a OneHotEncoder object ohe = OneHotEncoder() # Fit and transform the columns using the OneHotEncoder encoded_columns = ohe.fit_transform(X[columns_to_encode]) # Create a new DataFrame with the encoded columns encoded_df = pd.DataFrame(encoded_columns.toarray(), columns=ohe.get_feature_names_out(columns_to_encode)) # Drop the original columns from the original DataFrame X.drop(columns_to_encode, axis=1, inplace=True) # Concatenate the original DataFrame with the new encoded DataFrame X = pd.concat([X, encoded_df], axis=1) y = y.replace({'Yes': 1, 'No': 0}) columns_to_encode = ['TechSupport', 'Contract','PaymentMethod', 'PhoneServices', 'SecurityServices','StreamingServices', 'InternetServices'] # Create a OneHotEncoder object ohe = OneHotEncoder() # Fit and transform the columns using the OneHotEncoder encoded_columns = ohe.fit_transform(X[columns_to_encode]) # Create a new DataFrame with the encoded columns encoded_df = pd.DataFrame(encoded_columns.toarray(), columns=ohe.get_feature_names_out(columns_to_encode)) # Drop the original columns from the original DataFrame X.drop(columns_to_encode, axis=1, inplace=True) # Concatenate the original DataFrame with the new encoded DataFrame X = pd.concat([X, encoded_df], axis=1)
Масштабирование функций
from sklearn.preprocessing import MinMaxScaler # Create an instance of MinMaxScaler scaler = MinMaxScaler() # Fit and transform the MonthlyCharges and TotalCharges columns X[['MonthlyCharges', 'TotalCharges', 'tenure']] = scaler.fit_transform(X[['MonthlyCharges', 'TotalCharges', 'tenure']]) #X[['TotalCharges']] = scaler.fit_transform(X[['TotalCharges']])
Окончательные характеристики
<class 'pandas.core.frame.DataFrame'> RangeIndex: 7043 entries, 0 to 7042 Data columns (total 38 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 SeniorCitizen 7043 non-null int64 1 tenure 7043 non-null int64 2 MonthlyCharges 7043 non-null float64 3 TotalCharges 7043 non-null float64 4 Partner_No 7043 non-null float64 5 Partner_Yes 7043 non-null float64 6 Dependents_No 7043 non-null float64 7 Dependents_Yes 7043 non-null float64 8 PaperlessBilling_No 7043 non-null float64 9 PaperlessBilling_Yes 7043 non-null float64 10 gender_Female 7043 non-null float64 11 gender_Male 7043 non-null float64 12 TechSupport_No 7043 non-null float64 13 TechSupport_No internet service 7043 non-null float64 14 TechSupport_Yes 7043 non-null float64 15 Contract_Month-to-month 7043 non-null float64 16 Contract_One year 7043 non-null float64 17 Contract_Two year 7043 non-null float64 18 PaymentMethod_Bank transfer (automatic) 7043 non-null float64 19 PaymentMethod_Credit card (automatic) 7043 non-null float64 20 PaymentMethod_Electronic check 7043 non-null float64 21 PaymentMethod_Mailed check 7043 non-null float64 22 PhoneServices_MultipleLines 7043 non-null float64 23 PhoneServices_SingleLine 7043 non-null float64 24 SecurityServices_BothSecurityServices 7043 non-null float64 25 SecurityServices_No internet serviceNo internet service 7043 non-null float64 26 SecurityServices_NoneSecurityServices 7043 non-null float64 27 SecurityServices_OnlyDeviceProtection 7043 non-null float64 28 SecurityServices_OnlyOnlineSecurity 7043 non-null float64 29 StreamingServices_BothStreamingServices 7043 non-null float64 30 StreamingServices_No internet serviceNo internet service 7043 non-null float64 31 StreamingServices_NoneStreamingServices 7043 non-null float64 32 StreamingServices_OnlyStreamingMovies 7043 non-null float64 33 StreamingServices_OnlyStreamingTV 7043 non-null float64 34 InternetServices_DSL Only 7043 non-null float64 35 InternetServices_Fiber Optic Only 7043 non-null float64 36 InternetServices_Internet and Backup 7043 non-null float64 37 InternetServices_No Internet Service 7043 non-null float64 dtypes: float64(36), int64(2) memory usage: 2.0 MB
Тестирование на мультиколлинеарность
Мультиколлинеарность — это статистический термин, который описывает высокую корреляцию между двумя или более независимыми переменными в регрессионном анализе. Другими словами, это относится к ситуации, когда одна предикторная переменная может быть линейно предсказана по другим с существенной степенью точности. Это может вызвать проблемы в регрессионном анализе, поскольку может затруднить определение истинного влияния каждой независимой переменной на зависимую переменную и может привести к нестабильным и ненадежным оценкам коэффициентов регрессии. Мультиколлинеарность может возникнуть, когда независимые переменные сильно коррелируют друг с другом, когда в модели слишком много переменных или когда между предикторами существует функциональная связь.
Первый шаг – измерение корреляции:
Корреляция — это распространенный метод, используемый для обнаружения мультиколлинеарности в регрессионном анализе. Коэффициент корреляции измеряет силу и направление линейной зависимости между двумя переменными. В контексте мультиколлинеарности нас интересует корреляция между независимыми переменными.
Если корреляция между двумя независимыми переменными высока (т.е. близка к 1 или -1), это предполагает, что они сильно коррелированы, и в модели может быть мультиколлинеарность. Это может быть проблематично, поскольку затрудняет определение независимого влияния каждой переменной на зависимую переменную.
corr_matrix = X.corr() # Create a heatmap of the correlation matrix fig, ax = plt.subplots(figsize=(15, 15)) sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', ax=ax) plt.show()
Затем мы определяем, есть ли какие-либо пары переменных с высокой степенью корреляции, которые могут указывать на мультиколлинеарность и могут потребовать дальнейшего анализа.
# Create correlation matrix corr_matrix = churn_data.corr().abs() # Get pairs of columns with high correlation coefficients high_corr = [(i, j) for i in range(corr_matrix.shape[0]) for j in range(i+1, corr_matrix.shape[1]) if corr_matrix.iloc[i,j] > 0.5] # Print pairs of columns with high correlation coefficients for i, j in high_corr: print(f"{corr_matrix.index[i]} - {corr_matrix.columns[j]}: {corr_matrix.iloc[i,j]}")
Результаты указывают на следующие высокие корреляции.
Я пошел дальше, чтобы использовать собственные значения для оценки мультиколлинеарности между различными столбцами.
# Calculate eigenvalues eigvals, eigvecs = np.linalg.eig(corr_matrix) # Print eigenvalues print(eigvals) # Check for eigenvalues close to 0 eigen_tol = 1e-4 cols_to_drop = [] for i, val in enumerate(eigvals): if val < eigen_tol: cols_to_drop.append(corr_matrix.columns[i]) # Drop columns with high levels of multicollinearity if len(cols_to_drop) > 0: churn_data.drop(cols_to_drop, axis=1, inplace=True) print(f"Columns with high levels of multicollinearity: {cols_to_drop}") else: print("No columns with high levels of multicollinearity.")
Поскольку мы получили результаты, показывающие, что ни один столбец не обладает высокой мультиколлинеарностью, я решил сохранить все столбцы для дальнейшего анализа.
Тренировочный тестовый сплит
# Use train_test_split with a random_state, and add stratify for Classification from sklearn.model_selection import train_test_split # split the data into training and testing sets X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
Модель логистической регрессии со штрафом L1 (для Lasso) или штрафом L2 (для Ridge)
Логистическая регрессия с регуляризацией L1 или L2 может помочь улучшить производительность модели на несбалансированных наборах данных за счет уменьшения переобучения и повышения способности модели к обобщению.
Регуляризация L1 (лассо) также может выполнять выбор признаков, уменьшая коэффициенты менее важных признаков до нуля. Это может помочь улучшить производительность модели за счет уменьшения влияния зашумленных или нерелевантных функций, что может быть особенно важно в несбалансированных наборах данных, где класс меньшинства может быть связан с небольшим количеством важных функций. Я реализовал обе логистические регрессии с регуляризацией L1 или L2.
Для получения более подробной информации о реализации ознакомьтесь с моей записной книжкой на github. (Ссылка ниже).
И классификаторы Lasso, и Ridge имеют одинаковую точность, точность, полноту и оценку F1. Это может означать, что модель не переоснащает и не недообучает и хорошо обобщает тестовые данные. Однако значение точности довольно низкое (0,50), что указывает на то, что модель имеет высокий уровень ложных срабатываний, то есть она неверно предсказывает, что клиенты будут уходить, хотя на самом деле этого не произойдет.
Значение отзыва выше (0,77), что указывает на то, что модель имеет более низкий уровень ложноотрицательных результатов, т. е. она правильно предсказывает, что клиенты будут уходить, когда они действительно уйдут.
Показатель F1, который является гармоническим средним значением точности и полноты, также низок (0,61), что указывает на то, что модель может быть не лучшей с точки зрения баланса между точностью и полнотой.
В целом эти результаты показывают, что модель можно улучшить, используя другой алгоритм или настроив гиперпараметры текущего алгоритма. Кроме того, может быть полезно собрать больше данных или разработать дополнительные функции для повышения производительности модели.
Моделирование машинного обучения
Модель № 001
Классификатор RandomForest по несбалансированному набору данных
Для первой модели мы обучаем классификатор RandomForest на несбалансированном наборе данных. Для древовидных алгоритмов, таких как случайный лес, повышение градиента и деревья решений, масштабирование не требуется, поскольку на них не влияет масштаб входных объектов.
Модель №002
Дерево решений для несбалансированного набора данных
Для 2-й модели мы обучаем алгоритм дерева решений.
Модель №003
Адаптивное повышение (AdaBoost) по сравнению с несбалансированным набором данных
Для 3-й модели мы обучаем алгоритм Adaptive Boosting (AdaBoost).
Модель №004
Наивный байесовский анализ данных Balanced Train
Для 4-й модели мы обучаем наивный байесовский метод передискретизации синтетического меньшинства (SMOTE). Проблема с несбалансированной классификацией заключается в том, что существует слишком мало примеров класса меньшинства, чтобы модель могла эффективно изучить границу решения.
Один из способов решить эту проблему — передискретизировать примеры в миноритарном классе. Этого можно достичь, просто продублировав примеры из класса меньшинства в наборе обучающих данных перед подгонкой модели. Это может сбалансировать распределение классов, но не дает модели никакой дополнительной информации.
from imblearn.over_sampling import SMOTE # Create the SMOTE object smote = SMOTE() # Apply SMOTE to the training data only X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)
Модель №005
SVM по данным сбалансированного поезда
Для 5-й модели мы обучаем SVM. Для других алгоритмов, таких как SVM и K-ближайших соседей, может потребоваться масштабирование, чтобы гарантировать, что все функции находятся в одинаковом масштабе. Горячее кодирование может привести к большому количеству разреженных функций, что может затруднить масштабирование данных. В таких случаях методы выбора признаков или уменьшения размерности, такие как PCA, могут использоваться для уменьшения количества признаков перед масштабированием.
Модель №006
К ближайших соседей для сбалансированных данных поезда
Для 6-й модели мы обучаем K-ближайших соседей по сбалансированным данным обучения.
Модель №007
Классификатор повышения градиента по сравнению со сбалансированными данными поезда
Для 7-й модели мы обучаем классификатор повышения градиента по сбалансированным данным обучения.
Модель №008
Catboost по сравнению со сбалансированными данными о поездах
Для 8-й модели мы тренируем catboost по сбалансированным данным поезда.
Модель №009
xgboost по сравнению со сбалансированными данными о поездах
Для 9-й модели мы тренируем xgboost по сбалансированным данным поезда.
Модель №010
Адаптивное повышение (AdaBoost) по сравнению со сбалансированным набором данных
Для 10-й модели мы обучаем алгоритм Adaptive Boosting (AdaBoost). Этот алгоритм строит серию деревьев решений, где каждое последующее дерево пытается исправить ошибки, допущенные предыдущим деревом.
Сравнение моделей
В таблице показаны метрики оценки для различных моделей, используемых для прогнозирования оттока. Используемые метрики оценки: точность, воспроизводимость, полнота и оценка F1.
Точность моделей колеблется от 69,34% до 79,56%. Модель с наибольшей точностью — AdaBoost со сбалансированными данными.
Точность — это отношение истинных срабатываний к сумме истинных срабатываний и ложных срабатываний. Точность моделей колеблется от 44,97% до 63,69%. Модель с наивысшей точностью — AdaBoost со сбалансированными данными.
Отзыв — это отношение истинных положительных результатов к сумме истинных положительных и ложных отрицательных результатов. Отзыв моделей колеблется от 46,52% до 81,02%. Модель с самым высоким отзывом — это Наивный Байес со сбалансированными данными.
Оценка F1 представляет собой гармоническое среднее точности и полноты. Оценка F1 моделей колеблется от 47,15% до 62,60%. Модель с наивысшей оценкой F1 — Gradient Boosting со сбалансированными данными.
В целом представляется, что наиболее эффективными моделями являются модели, в которых используются сбалансированные данные и методы ансамбля, такие как AdaBoost, Gradient Boosting и XGBoost. Наивный Байес также хорошо работает с точки зрения отзыва, но имеет более низкую точность и оценку F1 по сравнению с другими моделями. Наихудшей моделью является KNN, которая имеет низкую точность, точность и оценку F1.
Настройка гиперпараметров.
GradientBoostingClassifier
Мой код будет выполнять поиск по сетке для настройки гиперпараметров в классификаторе Gradient Boosting с использованием библиотеки sklearn в Python. Настраиваемые гиперпараметры — это количество оценок, скорость обучения и максимальная глубина дерева. Оценка F1 используется в качестве метрики оценки для поиска по сетке. Поиск по сетке выполняется с использованием 5-кратной перекрестной проверки и будет использовать все доступные ядра ЦП.
# Fit the GridSearchCV object to the preprocessed training data grid_search.fit(X_train, y_train) # Print the best hyperparameters and the F1 score print("Best hyperparameters:", grid_search.best_params_) print("F1 score:", grid_search.best_score_)
# Predict on the test data y_pred_gboost_H = grid_search.predict(X_test) # Print the classification report print(classification_report(y_test, y_pred_gboost_H))
# Fit the GridSearchCV object to the training data grid_search.fit(X_train_resampled, y_train_resampled) # Print the best hyperparameters and the F1 score print("Best hyperparameters:", grid_search.best_params_) print("F1 score:", grid_search.best_score_)
Этот результат предполагает, что после настройки гиперпараметров с помощью GridSearchCV наилучшей комбинацией гиперпараметров для модели классификатора GradientBoosting являются скорость обучения 0,01, максимальная глубина 5 и 500 оценок. Оценка F1, полученная моделью с этими гиперпараметрами, составляет 0,8346, что является мерой точности модели, при этом более высокая оценка указывает на лучшую производительность. Это указывает на то, что производительность модели была улучшена за счет настройки гиперпараметров по сравнению с гиперпараметрами по умолчанию, используемыми в исходной модели.
# Predict on the test data y_pred_gboost_H_resampled = grid_search.predict(X_test) # Print the classification report print(classification_report(y_test, y_pred_gboost_H_resampled))
Основываясь на результатах, общая точность модели повышения градиента после настройки гиперпараметров составляет 76%, что означает, что модель правильно предсказала 76% случаев. Модель имеет более высокую точность для класса 0 (клиенты, которые не ушли) на 89%, а точность для класса 1 (клиенты, которые ушли) ниже на 53%. Отзыв, который измеряет способность модели правильно идентифицировать все положительные случаи, выше для класса 1 на 74%, а отзыв для класса 0 составляет 76%. Показатель F1, представляющий собой средневзвешенное значение точности и полноты, составляет 0,62 для класса 1 и 0,82 для класса 0. Макросредний показатель F1 составляет 0,72, что является средним значением показателей F1 для обоих классов. Средневзвешенная оценка F1 составляет 0,77, что учитывает дисбаланс количества случаев для каждого класса. В целом, модель, по-видимому, лучше прогнозирует клиентов, которые не ушли, по сравнению с теми, кто ушёл.
ОКОНЧАТЕЛЬНЫЙ ВЫБОР МОДЕЛИ
Первая модель имеет более высокую общую точность (0,80 против 0,77) и более высокий балл F1 для положительного класса (0,57 против 0,61), что указывает на то, что она лучше правильно прогнозирует случаи оттока. Однако вторая модель имеет более высокий балл F1 для отрицательного класса (0,84 против 0,87), что указывает на то, что она лучше правильно прогнозирует случаи отсутствия оттока.
В целом, если цель состоит в том, чтобы правильно определить приоритеты случаев оттока, первая модель может быть предпочтительнее. Если цель состоит в том, чтобы свести к минимуму ложные срабатывания (т. е. прогнозировать отток клиентов, когда его нет), может быть предпочтительнее вторая модель. В конечном итоге выбор между двумя моделями будет зависеть от конкретных потребностей проекта и компромисса между правильным определением оттока и минимизацией ложных срабатываний.
Ознакомьтесь с Частью 3, где я разрабатываю стратегии удержания на основе демографических данных и сегментации K-средних.
Наконец, проверьте мой github для полного кода.