Modelado de Churn para una Empresa de Telecomunicaciones
Author
Ing Gabriel Chávez Camargo
Published
October 14, 2024
PROBLEMA DE NEGOCIO
Una empresa de telecomunicaciones desea mejorar la retención de sus clientes, identificando aquellos que tienen más chances de abandonar el servicio (Churn). Como científico de datos, tu objetivo será limpiar y preparar un conjunto de datos para el entrenamiento de un modelo de Churn. A continuación, se presentan las preguntas clave que guiarán el proceso de limpieza de datos:
¿Qué insights podemos obtener del análisis exploratorio inicial del conjunto de datos?
¿Qué transformaciones básicas son necesarias para preparar los datos?
¿Cómo podemos identificar y tratar los datos duplicados y los valores nulos?
¿Cómo manejamos los outliers presentes en el dataset?
¿Qué técnicas aplicamos para procesar las variables categóricas?
Observamos que tenemos 7344 valores totales, pero adentro encontramos las diferentes columnas.
Code
# Conversion de datos dfdef lectura_datos(data):withopen ('base_clientes.json',encoding='utf-8') as f: json_bruto=json.load(f) datos_churn=pd.json_normalize(json_bruto)return datos_churndatos_churn=lectura_datos(datos_churn)
La base de datos contiene columnas además del ID de los clientes y el churn:
Cliente:
género: género (masculino y femenino)
anciano: información sobre si un cliente tiene o no una edad igual o mayor a 65 años
pareja: si el cliente tiene o no una pareja
dependientes: si el cliente tiene o no dependientes
tiempo_servicio: meses de contrato del cliente
Servicio de telefonía:
servicio_telefono: suscripción al servicio telefónico
varias_lineas: suscripción a más de una línea telefónica
Servicio de internet:
servicio_internet: suscripción a un proveedor de internet
seguridad_online: suscripción adicional a seguridad en línea
backup_online: suscripción adicional a copias de seguridad en línea
proteccion_dispositivo: suscripción adicional a protección en el dispositivo
soporte_tecnico: suscripción adicional a soporte técnico, menos tiempo de espera
tv_streaming: suscripción a TV por cable
peliculas_streaming: suscripción a streaming de películas
Cuenta:
contrato: tipo de contrato
factura_electronica: si el cliente prefiere recibir la factura en línea
metodo_pago: forma de pago
cobros_mensuales: total de todos los servicios del cliente por mes
cobros_totales: total gastado por el cliente
Obtenemos la descripción de Cada una de las columnas
La columna con mas valores faltantes se encuetra en cuenta.contrato con 1.5% de los datos.
Code
columnas=datos_churn.columnsfor columnas in columnas:print('Columna',columnas)print(datos_churn[columnas].value_counts())print('---------------------------------')
Columna id_cliente
id_cliente
0793-TWELN 2
7176-WIONM 2
2371-JQHZZ 2
1193-RTSLK 2
2192-CKRLV 2
..
3401-URHDA 1
3400-ESFUW 1
3399-BMLVW 1
3398-ZOUAA 1
9995-HOTOH 1
Name: count, Length: 7267, dtype: int64
---------------------------------
Columna Churn
Churn
no 5223
si 1895
226
Name: count, dtype: int64
---------------------------------
Columna cliente.genero
cliente.genero
masculino 3713
femenino 3631
Name: count, dtype: int64
---------------------------------
Columna cliente.anciano
cliente.anciano
0 6147
1 1197
Name: count, dtype: int64
---------------------------------
Columna cliente.pareja
cliente.pareja
no 3793
si 3551
Name: count, dtype: int64
---------------------------------
Columna cliente.dependientes
cliente.dependientes
no 5143
si 2201
Name: count, dtype: int64
---------------------------------
Columna cliente.tiempo_servicio
cliente.tiempo_servicio
1.0 639
72.0 374
2.0 249
3.0 209
4.0 188
...
321.0 1
254.0 1
1000.0 1
1080.0 1
512.0 1
Name: count, Length: 84, dtype: int64
---------------------------------
Columna telefono.servicio_telefono
telefono.servicio_telefono
si 6629
no 715
Name: count, dtype: int64
---------------------------------
Columna telefono.varias_lineas
telefono.varias_lineas
no 3535
si 3094
sin servicio de telefono 715
Name: count, dtype: int64
---------------------------------
Columna internet.servicio_internet
internet.servicio_internet
fibra optica 3234
DSL 2510
no 1600
Name: count, dtype: int64
---------------------------------
Columna internet.seguridad_online
internet.seguridad_online
no 3643
si 2101
sin servicio de internet 1600
Name: count, dtype: int64
---------------------------------
Columna internet.backup_online
internet.backup_online
no 3214
si 2530
sin servicio de internet 1600
Name: count, dtype: int64
---------------------------------
Columna internet.proteccion_dispositivo
internet.proteccion_dispositivo
no 3227
si 2517
sin servicio de internet 1600
Name: count, dtype: int64
---------------------------------
Columna internet.soporte_tecnico
internet.soporte_tecnico
no 3624
si 2120
sin servicio de internet 1600
Name: count, dtype: int64
---------------------------------
Columna internet.tv_streaming
internet.tv_streaming
no 2926
si 2818
sin servicio de internet 1600
Name: count, dtype: int64
---------------------------------
Columna internet.peliculas_streaming
internet.peliculas_streaming
no 2893
si 2851
sin servicio de internet 1600
Name: count, dtype: int64
---------------------------------
Columna cuenta.contrato
cuenta.contrato
mensual 4039
dos años 1755
un año 1518
Name: count, dtype: int64
---------------------------------
Columna cuenta.facturacion_electronica
cuenta.facturacion_electronica
si 4344
no 2982
Name: count, dtype: int64
---------------------------------
Columna cuenta.metodo_pago
cuenta.metodo_pago
cheque electronico 2459
cheque 1678
transferencia bancaria (automatica) 1600
tarjeta de credito (automatico) 1580
Name: count, dtype: int64
---------------------------------
Columna cuenta.cobros.mensual
cuenta.cobros.mensual
20.05 65
19.85 46
19.90 46
19.55 46
19.70 45
..
117.60 1
33.65 1
23.45 1
116.55 1
67.85 1
Name: count, Length: 1585, dtype: int64
---------------------------------
Columna cuenta.cobros.Total
cuenta.cobros.Total
20.2 11
11
19.55 10
19.9 9
19.75 9
..
499.4 1
1160.75 1
1396 1
435 1
3707.6 1
Name: count, Length: 6516, dtype: int64
---------------------------------
Encontramos algo interesante en la columna Cobros toal tenemos 11 columnas sin ningun tipo de valor al igual que nuestra variable objetivo CHURN tuene 226 registros vacios.
Podriamos suponer que las columna tiempo_servicio se podria rellenar con 24 que refiere a dos años pero no sabemos el cliente en realidad, cuanto tiempo tiene con la empresa.
Podemos observar que los nulos no tienen un patron claro para poder remplazarlos, y en algunos casos algunas columnas estan casi vacias.
Code
datos_churn.describe()
cliente.anciano
cliente.tiempo_servicio
cuenta.cobros.mensual
count
7344.000000
7336.000000
7326.000000
mean
0.162990
33.271265
64.683770
std
0.369382
35.776684
30.143033
min
0.000000
0.000000
18.250000
25%
0.000000
9.000000
35.362500
50%
0.000000
29.000000
70.300000
75%
0.000000
56.000000
89.887500
max
1.000000
1080.000000
118.750000
En la columna cliente.tiempo_servicio obtenemos usuarios con 180 meses lo veremos mejor en un BOXPLOT
Code
# Configurar el tamaño de la figuraplt.figure(figsize=(12, 6))# Crear los subplotsplt.subplot(1, 3, 1)sns.boxplot(x='cliente.anciano', data=datos_churn)plt.title('Boxplot de cliente.anciano')plt.subplot(1, 3, 2)sns.boxplot(x='cliente.tiempo_servicio', data=datos_churn)plt.title('Boxplot de cliente.tiempo_servicio')plt.subplot(1, 3, 3)sns.boxplot(x='cuenta.cobros.mensual', data=datos_churn)plt.title('Boxplot de cuenta.cobros.mensual')# Ajustar el layout para que no se superpongan los gráficosplt.tight_layout()# Mostrar el gráficoplt.show()
3 Tratamiento de Datos
3.1 Preprocesamiento
Code
def preprocesamiento(datos_churn):#=====================================================================#Limpieza de outliers#=====================================================================# Calcular cuartiles e IQR primer_cuartil = datos_churn['cliente.tiempo_servicio'].quantile(0.25) tercer_cuartil = datos_churn['cliente.tiempo_servicio'].quantile(0.75) IQR = tercer_cuartil - primer_cuartil# Calcular los límites para considerar los valores atípicos limite_inferior = primer_cuartil -1.5* IQR limite_superior = tercer_cuartil +1.5* IQR# Filtrar los valores que están dentro de los límites datos_churn = datos_churn[datos_churn['cliente.tiempo_servicio'].between(limite_inferior, limite_superior)]#=====================================================================#Relleno de los valores "" en las columnas con nan #=====================================================================#Primera columna en los datos churn cambairemos los valores '' por nan para despues eliminarlos. datos_churn.loc[:, 'Churn'] = datos_churn['Churn'].replace('', np.nan)#Segunda Columna en los datos cuenta.cobros.Total cambairemos los valores '' por nan para despues eliminarlos. datos_churn.loc[:, 'cuenta.cobros.Total'] = datos_churn['cuenta.cobros.Total'].replace(' ', np.nan)#=====================================================================#Limpieza de duplicados y nulos#===================================================================== datos_churn=datos_churn.dropna() datos_churn=datos_churn.drop_duplicates().reset_index(drop=True) datos_churn['cuenta.cobros.Total']=datos_churn['cuenta.cobros.Total'].astype('float')## Nueva variables agregadas datos_churn['ratio_gasto_tiempo'] = (datos_churn['cuenta.cobros.Total'] / datos_churn['cliente.tiempo_servicio']).round(2)# Calcula el gasto total adicional datos_churn['gasto_total_adicional'] = datos_churn['cuenta.cobros.Total'] - (datos_churn['cuenta.cobros.mensual'] * datos_churn['cliente.tiempo_servicio']) datos_churn['gasto_adicional'] = datos_churn['gasto_total_adicional'].apply(lambda x: x if x >0else0)# Elimina la columna 'gasto_total_adicional' si ya no es necesaria datos_churn.drop(columns=['gasto_total_adicional'], inplace=True)return datos_churn
#=====================================================================#Verificación de los Datos#=====================================================================datos_churn.info()# Configurar el tamaño de la figuraplt.figure(figsize=(15, 9))# Crear los subplotsplt.subplot(1, 3, 1)sns.boxplot(x='cuenta.cobros.Total', data=datos_churn)plt.title('Boxplot de cuenta.cobros.Total')plt.subplot(1, 3, 2)sns.boxplot(x='cliente.tiempo_servicio', data=datos_churn)plt.title('Boxplot de cliente.tiempo_servicio')plt.subplot(1, 3, 3)sns.boxplot(x='ratio_gasto_tiempo', data=datos_churn)plt.title('Boxplot de cliente.tiempo_servicio')plt.show()
Implementamos métodos de selección de características usando SelectKBest, RFE y PCA para reducir la dimensionalidad del conjunto de datos.
5.1 SelectKbest
Este método selecciona las mejores características según su relevancia estadística usando la prueba chi-cuadrado. Es útil cuando se trabaja con datos categóricos y selecciona las características más relevantes para el modelo.
Accuracy en prueba con SelectKBest y chi2: 0.8044412607449857
Tiempo de ejecución: 0.7632 segundos
5.2 RFE (Recursive Feature Elimination)
Este algoritmo selecciona características eliminando recursivamente las menos importantes según el modelo. Usa un clasificador (en este caso, RandomForest) para identificar y eliminar las características menos relevantes, dejando solo las más útiles.
Code
def pronosticar_RFE(train_x, test_x, train_y, test_y): start_time = time.time() model = RandomForestClassifier(random_state=50) mejor_n =0 mejor_score =0 mejores_columnas = []for n inrange(1, train_x.shape[1] +1): rfe = RFE(estimator=model, n_features_to_select=n) rfe.fit(train_x, train_y) train_x_rfe = rfe.transform(train_x)# Calcular el score con validación cruzada scores = cross_val_score(model, train_x_rfe, train_y, cv=5, scoring='accuracy') score = scores.mean()if score > mejor_score: mejor_n = n mejor_score = score mejores_columnas =list(train_x.columns[rfe.support_])# Imprimir resultadosprint(f"Mejor número de características: {mejor_n}")print(f"Mejor accuracy: {mejor_score:.4f}")print("Mejores columnas seleccionadas:", mejores_columnas)# Entrenar el modelo final con las mejores características mejor_model = RandomForestClassifier(random_state=50) mejor_model.fit(train_x[mejores_columnas], train_y)# Evaluar en el conjunto de prueba test_x_rfe = test_x[mejores_columnas] test_accuracy = mejor_model.score(test_x_rfe, test_y)print(f"Accuracy en el conjunto de prueba: {test_accuracy:.4f}")# Calcular tiempo de ejecución end_time = time.time()print(f"Tiempo de ejecución: {end_time - start_time:.4f} segundos")pronosticar_RFE(X_train, X_test, y_train, y_test)
Mejor número de características: 21
Mejor accuracy: 0.7922
Mejores columnas seleccionadas: ['cliente.genero', 'cliente.anciano', 'cliente.pareja', 'cliente.dependientes', 'cliente.tiempo_servicio', 'telefono.servicio_telefono', 'telefono.varias_lineas', 'internet.servicio_internet', 'internet.seguridad_online', 'internet.backup_online', 'internet.proteccion_dispositivo', 'internet.soporte_tecnico', 'internet.tv_streaming', 'internet.peliculas_streaming', 'cuenta.contrato', 'cuenta.facturacion_electronica', 'cuenta.metodo_pago', 'cuenta.cobros.mensual', 'cuenta.cobros.Total', 'ratio_gasto_tiempo', 'gasto_adicional']
Accuracy en el conjunto de prueba: 0.7951
Tiempo de ejecución: 377.8390 segundos
5.3 PCA (Principal Component Analysis):
Este método transforma las características originales en un conjunto de nuevas variables no correlacionadas llamadas componentes principales, que capturan la mayor parte de la variabilidad de los datos, reduciendo la dimensionalidad sin perder mucha información.
Code
def pronosticar_PCA(train_x, test_x, train_y, test_y,): n =12 pca = PCA(n_components=n)# Ajustar y transformar los datos con PCA train_x_pca = pca.fit_transform(train_x) model = RandomForestClassifier(random_state=50) model.fit(train_x_pca, train_y) test_x_pca = pca.transform(test_x)# Imprimir resultados de PCAprint("Utilizando PCA:")print(f"El número de características seleccionadas es {n} con un score de {model.score(test_x_pca, test_y) *100:.2f}%")# Calcular y mostrar el total de varianza explicada total_varianza_explicada =sum(pca.explained_variance_ratio_)print(f"Total de varianza explicada por el PCA: {total_varianza_explicada *100:.2f}%")# Visualizar el explained variance ratio plt.figure(figsize=(10, 6)) plt.bar(range(1, n +1), pca.explained_variance_ratio_, alpha=0.7, color='blue') plt.ylabel('Explained Variance Ratio') plt.xlabel('Componentes Principales') plt.title('Proporción de Varianza Explicada por Componente Principal') plt.xticks(np.arange(1, n +1, 1)) plt.grid(axis='y', linestyle='--', alpha=0.7) plt.show()# Obtener las cargas de los componentes loadings = pca.components_.T * np.sqrt(pca.explained_variance_) component_names = [f'PC{i+1}'for i inrange(n)] loadings_df = pd.DataFrame(loadings, index=train_x.columns, columns=component_names)# Imprimir las cargas de los componentesprint("Cargas de los Componentes:")print(loadings_df.round(3))pronosticar_PCA(X_train, X_test, y_train, y_test)