Al operador de telecomunicaciones Interconnect le gustaría poder pronosticar su tasa de cancelación de clientes. Si se descubre que un usuario o usuaria planea irse, se le ofrecerán códigos promocionales y opciones de planes especiales. El equipo de marketing de Interconnect ha recopilado algunos de los datos personales de sus clientes, incluyendo información sobre sus planes y contratos.
Objetivo
Característica objetivo: la columna 'EndDate' es igual a 'No'.
Métrica principal: AUC-ROC. >75
Plan de Trabajo
Entendimiento del problema.
Comprender el problema a abordar y asegurar de tener acceso a datos relevantes y de buena calidad que permitan construir un modelo predictivo.
Preprocesamiento de los datos.
Preparar los datos para el modelado
Seleccionar modelo y entrenamiento.
Eligir un algoritmo de aprendizaje supervisado adecuado según el tipo de problema en este caso clasificación.
Evaluacion del modelo
Métrica principal: AUC-ROC.
Métrica adicional: exactitud.
Mejora del modelo
Si el modelo no cumple con los criterios de rendimiento deseados, considera técnicas como la optimización de hiperparámetros y la selección de características.
1. Configuración del Ambiente
Code
import warningsimport numpy as npimport pandas as pdimport seaborn as snsimport matplotlib.pyplot as pltfrom imblearn.over_sampling import RandomOverSamplerimport sklearn.metrics as metricsfrom sklearn.model_selection import train_test_split, GridSearchCVfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.metrics import roc_auc_score, roc_curvefrom sklearn.experimental import enable_iterative_imputerfrom sklearn.impute import KNNImputer, IterativeImputerfrom sklearn.preprocessing import OrdinalEncoder, StandardScalerfrom catboost import CatBoostClassifierfrom lightgbm import LGBMClassifier# Opciones de configuración de pandaspd.set_option('display.max_columns', None)import warnings
Visualización rápida todo parece correcto, verificaremos nulos y duplicados.
En las diferentes tablas podemos observar que existen más datos en unas tablas pero visualizaremos más estas cuestiones.
Code
# Esta función solo verifica los nulo y duplicados del DFdef verificar_nulos_duplicados(data,nombre_df):print(f'Nulos de {nombre_df}: \n{data.isna().sum()}')print('-------------------------------------------')print(f'Duplicados en {nombre_df}: {data.duplicated().sum()}')print(f'Duplicados en la llave customerID de {nombre_df}: {data["customerID"].duplicated().sum()}')
Nulos de internet:
customerID 0
InternetService 0
OnlineSecurity 0
OnlineBackup 0
DeviceProtection 0
TechSupport 0
StreamingTV 0
StreamingMovies 0
dtype: int64
-------------------------------------------
Duplicados en internet: 0
Duplicados en la llave customerID de internet: 0
Code
verificar_nulos_duplicados(df_phone,'phone')
Nulos de phone:
customerID 0
MultipleLines 0
dtype: int64
-------------------------------------------
Duplicados en phone: 0
Duplicados en la llave customerID de phone: 0
En cuestion de nulos y duplicados no tenemos en los data frames.
Pero verificaremos, la tabla de df_contract ya que habia columnas que se ecnotraban en formato diferente.
Como podemos observar tenemos un problema en esta columna al convertirla ValueError: could not convert string to float: ’ ’
fig, axes = plt.subplots(ncols=2, figsize=(15, 6))# Boxplot para 'budget' en el primer subplotsns.boxplot(x=df_contract['MonthlyCharges'], ax=axes[0])axes[0].set_title('Box Plot Cargos mensuales')axes[0].set_xlabel('Cargos mensuales')axes[0].set_ylabel('')# Boxplot para 'revenue' en el segundo subplotsns.boxplot(x=df_contract['TotalCharges'], ax=axes[1])axes[1].set_title('Box Plot Cargos Totales')axes[1].set_xlabel('Cargos Totales')axes[1].set_ylabel('')# Ajustar el espaciado y mostrar los gráficosplt.tight_layout()plt.show()
Encontramos que los cargos mensuale la mayoria de los usuarios se encuentran en los 70$
En cuestion de los Cargos Totales se encuentran al rededor de 1600 a 2000
Code
metodo_pago=df_contract['PaymentMethod'].value_counts().sort_values(ascending=False)fig, ax = plt.subplots(figsize=(13, 7))# Crear la gráfica de barrassns.barplot(x=metodo_pago.index, y=metodo_pago.values, ax=ax)# Agregar el texto de la conclusión al lado de la gráficaconclusion_text = ("El análisis de los pagos nos indica que \n""los usuarios prefieren el pago por \n""E-check.\n\n""Manteniendose los otros metodos de\n""pago a la par\n")ax.set_xlim(-0.5, len(metodo_pago) -0.5+2)text_x =len(metodo_pago) +0.7text_y = metodo_pago.max() /2# Agregar el texto en la posición calculadaax.text(text_x, text_y,conclusion_text, ha="center",va="center", fontsize=14)# Ajustar el espaciado entre los subplotsplt.tight_layout()ax.set_title("Payment Method", loc="center", fontdict={"fontsize":20}, pad=20)ax.set_xlabel("Payment Method")ax.spines['top'].set_visible(False)ax.spines['right'].set_visible(False)ax.spines['bottom'].set_visible(False)ax.spines['left'].set_visible(False)sns.despine(left=True, bottom=True)
Code
df_contract['cancel']=df_contract['EndDate'].apply(lambda x: '0'if x =='No'else'1')df_contract.head()
customerID
BeginDate
EndDate
Type
PaperlessBilling
PaymentMethod
MonthlyCharges
TotalCharges
cancel
0
7590-VHVEG
2020-01-01
No
Month-to-month
Yes
Electronic check
29.85
29.85
0
1
5575-GNVDE
2017-04-01
No
One year
No
Mailed check
56.95
1889.50
0
2
3668-QPYBK
2019-10-01
2019-12-01 00:00:00
Month-to-month
Yes
Mailed check
53.85
108.15
1
3
7795-CFOCW
2016-05-01
No
One year
No
Bank transfer (automatic)
42.30
1840.75
0
4
9237-HQITU
2019-09-01
2019-11-01 00:00:00
Month-to-month
Yes
Electronic check
70.70
151.65
1
Code
cancelation=df_contract['cancel'].value_counts().sort_values(ascending=False)fig, ax = plt.subplots(figsize=(8, 5))# Crear la gráfica de barrasbars=sns.barplot(x=cancelation.index, y=cancelation.values, ax=ax)# Agregar el texto de la conclusión al lado de la gráficaconclusion_text = ("El análisis de los contratos \n""nos indica que contamos con \n""un 26% de contratos finalizados \n""recordando que las fechas son \n""2019-10-01 al 2020-01-01\n""estos aumentaron en muy pocos meses.")total = cancelation.sum()percentages = [(count / total) *100for count in cancelation]# Añadir etiquetas de porcentaje en cada barrafor bar, percentage inzip(bars.patches, percentages): text_x = bar.get_x() + bar.get_width() /2 text_y = bar.get_height() text =f'{percentage:.1f}%' ax.text(text_x, text_y, text, ha='center', va='bottom', fontsize=12)ax.set_xlim(-0.5, len(cancelation) -0.5+2)text_x =len(cancelation) +0.7text_y = cancelation.max() /2# Agregar el texto en la posición calculadaax.text(text_x, text_y,conclusion_text, ha="center",va="center", fontsize=14)# Ajustar el espaciado entre los subplotsplt.tight_layout()ax.set_title("Agreement", loc="center", fontdict={"fontsize":20}, pad=20)ax.set_xlabel("")ax.spines['top'].set_visible(False)ax.spines['right'].set_visible(False)ax.spines['bottom'].set_visible(False)ax.spines['left'].set_visible(False)sns.despine(left=True, bottom=True)# Crear etiquetas personalizadas para la leyendalegend_labels = [ '0: Current','1: Cancelation']# Añadir la leyendaax.legend(legend_labels, loc='upper right', fontsize=12)# Mostrar la gráficaplt.show()
Podemos observar que hay un equilibrio entre los contratos a nombre de hombres y mujeres.
En relación con los contratos de adultos mayores, notamos que son menos frecuentes.
En cuanto a la existencia de una pareja, encontramos un equilibrio, lo que puede indicar una relación con los contratos entre hombres y mujeres.
Finalmente, en cuanto a las personas dependientes, como familiares o hijos, la mayoría de las personas no tienen dependientes.
Code
InternetService=df_internet['InternetService'].value_counts()fig, ax = plt.subplots(figsize=(8, 5))# Crear la gráfica de barrasbars=sns.barplot(x=InternetService.index, y=InternetService.values, ax=ax)# Agregar el texto de la conclusión al lado de la gráficaconclusion_text = ("El análisis del servicio de internet \n""nos indica que los usuarios \n""prefieren la Fibra Óptica\n")total = InternetService.sum()percentages = [(count / total) *100for count in InternetService]# Añadir etiquetas de porcentaje en cada barrafor bar, percentage inzip(bars.patches, percentages): text_x = bar.get_x() + bar.get_width() /2 text_y = bar.get_height() text =f'{percentage:.1f}%' ax.text(text_x, text_y, text, ha='center', va='bottom', fontsize=12)ax.set_xlim(-0.5, len(InternetService) -0.5+2)text_x =len(InternetService) +0.7text_y = InternetService.max() /2# Agregar el texto en la posición calculadaax.text(text_x, text_y,conclusion_text, ha="center",va="center", fontsize=14)# Ajustar el espaciado entre los subplotsplt.tight_layout()ax.set_title("Internet Service", loc="center", fontdict={"fontsize":20}, pad=20)ax.set_xlabel("")ax.spines['top'].set_visible(False)ax.spines['right'].set_visible(False)ax.spines['bottom'].set_visible(False)ax.spines['left'].set_visible(False)sns.despine(left=True, bottom=True)# Crear etiquetas personalizadas para la leyendalegend_labels = [ 'Fribra Optica','DSL']# Añadir la leyendaax.legend(legend_labels, loc='upper right', fontsize=12)# Mostrar la gráficaplt.show()
Code
# Crear un DataFrame con la estructura adecuada para graficar barras apiladasservices_counts = df_internet.iloc[:, 2:].apply(pd.Series.value_counts)fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(15, 10))for i, col inenumerate(services_counts.columns): ax = axes[i //3, i %3] services_counts[col].plot(kind='bar', ax=ax, color=['skyblue', 'salmon']) ax.set_title(col) ax.set_xlabel('Contrato') ax.set_ylabel('Número de clientes') ax.set_xticklabels(['Yes', 'No'], rotation=0) ax.grid(axis='y')# Ajustar el espaciado entre subplotsplt.tight_layout()# Mostrar la gráficaplt.show()
Podemos observar que la mayotia de los clientes prefiere los servicios
Seguridad en Internet: software antivirus (ProtecciónDeDispositivo) y un bloqueador de sitios web maliciosos (SeguridadEnLínea).
Podemos observar que algunos de estos contratos mantienen una relacion.
Tenemos que todos estos servicios cuentan arriba de 2500 usuarios cada uno.
Code
MultipleLines=df_phone['MultipleLines'].value_counts()fig, ax = plt.subplots(figsize=(8, 5))# Crear la gráfica de barrasbars=sns.barplot(x=MultipleLines.index, y=MultipleLines.values, ax=ax)# Agregar el texto de la conclusión al lado de la gráficaconclusion_text = ("El análisis sobre múltiples líneas \n""telefónicas nos indica que \n""los usuarios cuentan con solo \n""una línea telefónica\n\n""Anque el servicio de multiples líneas\n""es usado en un 46%.")total = MultipleLines.sum()percentages = [(count / total) *100for count in MultipleLines]# Añadir etiquetas de porcentaje en cada barrafor bar, percentage inzip(bars.patches, percentages): text_x = bar.get_x() + bar.get_width() /2 text_y = bar.get_height() text =f'{percentage:.1f}%' ax.text(text_x, text_y, text, ha='center', va='bottom', fontsize=12)ax.set_xlim(-0.5, len(MultipleLines) -0.5+2)text_x =len(MultipleLines) +0.7text_y = MultipleLines.max() /2# Agregar el texto en la posición calculadaax.text(text_x, text_y,conclusion_text, ha="center",va="center", fontsize=14)# Ajustar el espaciado entre los subplotsplt.tight_layout()ax.set_title("Multiple Lines", loc="center", fontdict={"fontsize":20}, pad=20)ax.set_xlabel("")ax.spines['top'].set_visible(False)ax.spines['right'].set_visible(False)ax.spines['bottom'].set_visible(False)ax.spines['left'].set_visible(False)sns.despine(left=True, bottom=True)# Mostrar la gráficaplt.show()
Pregunta 1. ¿Cuál fue la frecuencia de cancelación por genero?
Code
cancelacion_usuarios=df_contract.merge(df_personal,on='customerID',how='outer')cancel_counts = cancelacion_usuarios.groupby('gender')['cancel'].value_counts().unstack()# Graficar barras agrupadasax = cancel_counts.plot(kind='bar', stacked=False, color=['skyblue', 'salmon'], figsize=(8, 6))# Agregar los valores de las barrasfor p in ax.patches: ax.annotate(str(p.get_height()), (p.get_x() + p.get_width() /2., p.get_height()), ha='center', va='center', xytext=(0, 10), textcoords='offset points')# Personalización del gráficoplt.title('Cancelación por Género')plt.xlabel('Género')plt.ylabel('Número de Personas')plt.xticks(rotation=0)plt.legend(title='Cancelado', labels=['No', 'Sí'])# Mostrar la gráficaplt.tight_layout()plt.show()
Como podemos observar tenemos 900 cancelaciones tanto de usuarios hombres como de mujeres recordando que esto paso en un lapso de 4 meses
Pregunta 2.¿Qué Método de Pago obtuvo más cancelaciones?
Code
cancel_metod = cancelacion_usuarios.groupby('PaymentMethod')['cancel'].value_counts().unstack().drop(columns='0')# Graficar barras verticalesax = cancel_metod['1'].plot(kind='bar', color='skyblue', figsize=(10, 6))# Agregar texto al lado de las barrasfor p in ax.patches: ax.annotate(str(p.get_height()), (p.get_x() + p.get_width() /2., p.get_height()), ha='center', va='center', xytext=(0, 10), textcoords='offset points')# Personalización adicional del gráficoplt.title('Cancelaciones por Método de Pago')plt.xlabel('Método de Pago')plt.ylabel('Número de Cancelaciones')plt.xticks(rotation=45, ha='right')# Agregar texto de conclusiónconclusion_text = ("El método de pago con más cancelaciones fue\n""Electronic check\n")plt.text(0.99, 0.600, conclusion_text, ha='center', va='center', transform=ax.transAxes, fontsize=12)# Eliminar bordes del gráfico y cuadrículassns.despine(left=True, bottom=True)plt.grid(False)# Mostrar la gráficaplt.tight_layout()plt.show()
Pregunta 3.¿Cuál servicio obtuvo más cancelaciones?
Code
cancel_internet_service = cancelacion_servicios.groupby('InternetService')['cancel'].value_counts().unstack().drop(columns='0')cancelaciones = cancel_internet_service['1'].valuesservicios = cancel_internet_service.index.tolist()# Graficar el gráfico de pastelplt.figure(figsize=(8, 6))plt.pie(cancelaciones, labels=servicios, autopct='%1.1f%%', startangle=140, colors=['skyblue', 'salmon'])# Personalización del gráficoplt.title('Cancelaciones por Servicio de Internet')plt.axis('equal') # Para que el gráfico de pastel sea un círculo en lugar de elipse# Anotar el servicio con más cancelacionesmax_cancelaciones =max(cancelaciones)indice_max = cancelaciones.tolist().index(max_cancelaciones)servicio_max = servicios[indice_max]# Agregar texto de conclusiónconclusion_text = ("Fibra Óptica\n""contiene el mayor porcentaje de\n""cancelaciones\n")plt.text(0.99, 0.600, conclusion_text, ha='center', va='center', transform=ax.transAxes, fontsize=12)# Mostrar la gráficaplt.tight_layout()plt.show()
Pregunta 4 ¿Qué mes obtuvo más cancelaciones?
Code
cancel_services = cancelacion_servicios.groupby('Type')['cancel'].value_counts().unstack().drop(columns='0')ax = cancel_services['1'].plot(kind='bar', color='skyblue', figsize=(10, 6))# Agregar texto al lado de las barrasfor p in ax.patches: ax.annotate(str(p.get_height()), (p.get_x() + p.get_width() /2., p.get_height()), ha='center', va='center', xytext=(0, 10), textcoords='offset points')# Personalización adicional del gráficoplt.title('Cancelaciones por Mes')plt.xlabel('Mes')plt.ylabel('Número de Cancelaciones')plt.xticks(rotation=45, ha='right')# Agregar texto de conclusiónconclusion_text = ("El un pago mensual\n""fue el que tuvo mayor cancelación\n")plt.text(0.99, 0.600, conclusion_text, ha='center', va='center', transform=ax.transAxes, fontsize=12)# Eliminar bordes del gráfico y cuadrículassns.despine(left=True, bottom=True)plt.grid(False)# Mostrar la gráficaplt.tight_layout()plt.show()
En este caso tenemos que justamente nuestras caracteristicas que son las que nos pueden ayudan a tener mejor prediccion son las que cuentran con un 21% de los valores nulos
Al hacer el filtrado de solamente de los valores NULOS contamos que solamente un 12% de los registros fueron aquellos que cancelaron. Lo cual es demasiado poco y justamente es nuestra variable a predecir y no contiene suficientes registros en donde los clientes cancelaron algun tipo de plan.
Pero si eliminamos estos valores nulos es posible que perdamos un 20% de los datos lo cual no es muy conveniente ya que la base de datos no es tan grande.
Estos clientes cuentan solo con un serivico ya sea internet o télefono fijo, por lo cual no cuentan con ningun otro servicio extra.
haremos una imputacion por el metodo de knn vecinos mas cercanos, para no perder los datos.
Recordando que tambien pudieramos utilizar la moda, para estas variables categóricas.
Code
imputer_kk=KNNImputer(n_neighbors=5)#Creamos el data frame con los valores imputados nanimputed_X_train=pd.DataFrame(imputer_kk.fit_transform(X_train))imputed_X_valid=pd.DataFrame(imputer_kk.transform(X_test))#Obetnemos el nombre de las columnas imputed_X_train.columns=X_train.columnsimputed_X_valid.columns=X_test.columnscolumns_to_int = ['OnlineSecurity', 'OnlineBackup', 'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies', 'MultipleLines']#El método round(0) redondea al entero más cercano.imputed_X_train[columns_to_int] = imputed_X_train[columns_to_int].round(0).astype('Int64')imputed_X_valid[columns_to_int]= imputed_X_valid[columns_to_int].round(0).astype('Int64')X_train=imputed_X_train.copy()X_test=imputed_X_valid.copy()X_test.info()
En este caso optaremos por utilizar RandomOverSampler para tratar la información desbalanceada de nuestro data set haciendolo solo el conjunto de entreamiento. Ya que si lo aplicamos completamente a todo nuestro dataset.Estaremos haciendo una mala practica llamada data leakage
train test
Accuracy 0.888 0.770
ROC AUC 0.951 0.846
Podemos observar que obtenemos una tasa de verdaderos positivos de un 85%
Modelo 3 CatBoostClassifier
Code
# Definir el modelo CatBoostClassifiercatboost = CatBoostClassifier(random_state=12345, verbose=0, iterations=1000,learning_rate=0.01,l2_leaf_reg=2, scale_pos_weight=1,bagging_temperature=0.1)# Entrenar el modelo con los datos de entrenamiento balanceadoscatboost.fit(X_train_res, y_train_res)evaluate_model(catboost, X_train_res, y_train_res, X_test, y_test)
train test
Accuracy 0.827 0.765
ROC AUC 0.908 0.859
Podemos observar que obtenemos una tasa de verdaderos positivos de un 86%
Conclusión
El operador de telecomunicaciones Interconnect desea pronosticar la tasa de cancelación de sus clientes. La variable objetivo es cancel, donde 0 indica que el cliente aún tiene el plan y 1 que ha cancelado el servicio. El propósito es identificar a los clientes que planean cancelar para ofrecerles códigos promocionales y planes especiales, con el objetivo de reducir la tasa de cancelación.
Modelos Utilizados: Para abordar este problema, se implementaron tres modelos de clasificación: RandomForestClassifier, LGBMClassifier y CatBoostClassifier. A continuación, se presentan las métricas de rendimiento para cada modelo en los conjuntos de entrenamiento y prueba:
Modelo
Train Accuracy
Test Accuracy
Train ROC AUC
Test ROC AUC
RandomForestClassifier
0.834
0.774
0.911
0.858
LGBMClassifier
0.884
0.767
0.950
0.851
CatBoostClassifier
0.829
0.767
0.909
0.860
Análisis de Resultados:
El CatBoostClassifier parece ser el mejor modelo en términos de capacidad de generalización y predicción, con un ROC AUC en el conjunto de prueba de 0.860, ligeramente superior al de RandomForestClassifier. Aunque RandomForestClassifier tiene una Test Accuracy marginalmente mejor, la ligera ventaja de CatBoostClassifier en ROC AUC hace que sea una mejor opción para clasificaciones precisas en este contexto.