TELECOM

Author

Ing Gabriel Chávez Camargo

Published

July 15, 2024

PROBLEMA DE NEGOCIO

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 warnings

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

from imblearn.over_sampling import RandomOverSampler
import sklearn.metrics as metrics
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, roc_curve
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import KNNImputer, IterativeImputer
from sklearn.preprocessing import OrdinalEncoder, StandardScaler
from catboost import CatBoostClassifier
from lightgbm import LGBMClassifier

# Opciones de configuración de pandas
pd.set_option('display.max_columns', None)

import warnings

1. Tratamiento y Análisis de Exploratorio (EDA)

Code
df_contract=pd.read_csv('./csv/contract.csv')
df_contract.head()
customerID BeginDate EndDate Type PaperlessBilling PaymentMethod MonthlyCharges TotalCharges
0 7590-VHVEG 2020-01-01 No Month-to-month Yes Electronic check 29.85 29.85
1 5575-GNVDE 2017-04-01 No One year No Mailed check 56.95 1889.5
2 3668-QPYBK 2019-10-01 2019-12-01 00:00:00 Month-to-month Yes Mailed check 53.85 108.15
3 7795-CFOCW 2016-05-01 No One year No Bank transfer (automatic) 42.30 1840.75
4 9237-HQITU 2019-09-01 2019-11-01 00:00:00 Month-to-month Yes Electronic check 70.70 151.65
Code
df_contract.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 8 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   BeginDate         7043 non-null   object 
 2   EndDate           7043 non-null   object 
 3   Type              7043 non-null   object 
 4   PaperlessBilling  7043 non-null   object 
 5   PaymentMethod     7043 non-null   object 
 6   MonthlyCharges    7043 non-null   float64
 7   TotalCharges      7043 non-null   object 
dtypes: float64(1), object(7)
memory usage: 440.3+ KB
Code
df_personal=pd.read_csv('./csv/personal.csv')
#Datos personales del cliente 
df_personal.head()
customerID gender SeniorCitizen Partner Dependents
0 7590-VHVEG Female 0 Yes No
1 5575-GNVDE Male 0 No No
2 3668-QPYBK Male 0 No No
3 7795-CFOCW Male 0 No No
4 9237-HQITU Female 0 No No
Code
df_personal.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 5 columns):
 #   Column         Non-Null Count  Dtype 
---  ------         --------------  ----- 
 0   customerID     7043 non-null   object
 1   gender         7043 non-null   object
 2   SeniorCitizen  7043 non-null   int64 
 3   Partner        7043 non-null   object
 4   Dependents     7043 non-null   object
dtypes: int64(1), object(4)
memory usage: 275.2+ KB
Code
#Información sobre los servicios de internet 
df_internet=pd.read_csv('./csv/internet.csv')
df_internet.head()
customerID InternetService OnlineSecurity OnlineBackup DeviceProtection TechSupport StreamingTV StreamingMovies
0 7590-VHVEG DSL No Yes No No No No
1 5575-GNVDE DSL Yes No Yes No No No
2 3668-QPYBK DSL Yes Yes No No No No
3 7795-CFOCW DSL Yes No Yes Yes No No
4 9237-HQITU Fiber optic No No No No No No
Code
df_internet.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5517 entries, 0 to 5516
Data columns (total 8 columns):
 #   Column            Non-Null Count  Dtype 
---  ------            --------------  ----- 
 0   customerID        5517 non-null   object
 1   InternetService   5517 non-null   object
 2   OnlineSecurity    5517 non-null   object
 3   OnlineBackup      5517 non-null   object
 4   DeviceProtection  5517 non-null   object
 5   TechSupport       5517 non-null   object
 6   StreamingTV       5517 non-null   object
 7   StreamingMovies   5517 non-null   object
dtypes: object(8)
memory usage: 344.9+ KB
Code
df_phone=pd.read_csv('./csv/phone.csv')
df_phone.head()
customerID MultipleLines
0 5575-GNVDE No
1 3668-QPYBK No
2 9237-HQITU No
3 9305-CDSKC Yes
4 1452-KIOVK Yes
Code
df_phone.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6361 entries, 0 to 6360
Data columns (total 2 columns):
 #   Column         Non-Null Count  Dtype 
---  ------         --------------  ----- 
 0   customerID     6361 non-null   object
 1   MultipleLines  6361 non-null   object
dtypes: object(2)
memory usage: 99.5+ KB

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 DF
def 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()}')
Code
verificar_nulos_duplicados(df_contract,'contract')
Nulos de contract: 
customerID          0
BeginDate           0
EndDate             0
Type                0
PaperlessBilling    0
PaymentMethod       0
MonthlyCharges      0
TotalCharges        0
dtype: int64
-------------------------------------------
Duplicados en contract:             0
Duplicados en la llave customerID de contract: 0
Code
verificar_nulos_duplicados(df_personal,'personal')
Nulos de personal: 
customerID       0
gender           0
SeniorCitizen    0
Partner          0
Dependents       0
dtype: int64
-------------------------------------------
Duplicados en personal:             0
Duplicados en la llave customerID de personal: 0
Code
verificar_nulos_duplicados(df_internet,'internet')
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: ’ ’

df_contract['TotalCharges']=df_contract['TotalCharges'].astype('float')
Code
df_contract['TotalCharges'].value_counts()
TotalCharges
          11
20.2      11
19.75      9
20.05      8
19.9       8
          ..
6849.4     1
692.35     1
130.15     1
3211.9     1
6844.5     1
Name: count, Length: 6531, dtype: int64
Code
df_contract[df_contract['TotalCharges']==' '].head()
customerID BeginDate EndDate Type PaperlessBilling PaymentMethod MonthlyCharges TotalCharges
488 4472-LVYGI 2020-02-01 No Two year Yes Bank transfer (automatic) 52.55
753 3115-CZMZD 2020-02-01 No Two year No Mailed check 20.25
936 5709-LVOEQ 2020-02-01 No Two year No Mailed check 80.85
1082 4367-NUYAO 2020-02-01 No Two year No Mailed check 25.75
1340 1371-DWPAZ 2020-02-01 No Two year No Credit card (automatic) 56.05
Code
df_contract['TotalCharges'] = pd.to_numeric(df_contract['TotalCharges'], errors='coerce').astype('float')
Code
df_contract['BeginDate']=pd.to_datetime(df_contract['BeginDate'])
df_contract['BeginDate'].dt.year.value_counts()
BeginDate
2019    1957
2014    1344
2018    1030
2015     852
2017     845
2016     763
2020     244
2013       8
Name: count, dtype: int64
Code
df_contract['EndDate'].value_counts()
EndDate
No                     5174
2019-11-01 00:00:00     485
2019-12-01 00:00:00     466
2020-01-01 00:00:00     460
2019-10-01 00:00:00     458
Name: count, dtype: int64
Code
df_contract.describe()
BeginDate MonthlyCharges TotalCharges
count 7043 7043.000000 7032.000000
mean 2017-04-30 13:01:50.918642688 64.761692 2283.300441
min 2013-10-01 00:00:00 18.250000 18.800000
25% 2015-06-01 00:00:00 35.500000 401.450000
50% 2017-09-01 00:00:00 70.350000 1397.475000
75% 2019-04-01 00:00:00 89.850000 3794.737500
max 2020-02-01 00:00:00 118.750000 8684.800000
std NaN 30.090047 2266.771362
Code
fig, axes = plt.subplots(ncols=2, figsize=(15, 6))

# Boxplot para 'budget' en el primer subplot
sns.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 subplot
sns.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áficos
plt.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 barras
sns.barplot(x=metodo_pago.index, y=metodo_pago.values, ax=ax)

# Agregar el texto de la conclusión al lado de la gráfica
conclusion_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.7
text_y = metodo_pago.max() / 2

# Agregar el texto en la posición calculada
ax.text(text_x, text_y,conclusion_text, ha="center",va="center", fontsize=14)
# Ajustar el espaciado entre los subplots
plt.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 barras
bars=sns.barplot(x=cancelation.index, y=cancelation.values, ax=ax)

# Agregar el texto de la conclusión al lado de la gráfica
conclusion_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) * 100 for count in cancelation]
# Añadir etiquetas de porcentaje en cada barra
for bar, percentage in zip(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.7
text_y = cancelation.max() / 2

# Agregar el texto en la posición calculada
ax.text(text_x, text_y,conclusion_text, ha="center",va="center", fontsize=14)
# Ajustar el espaciado entre los subplots
plt.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 leyenda
legend_labels = [ '0: Current','1: Cancelation']
# Añadir la leyenda
ax.legend(legend_labels, loc='upper right', fontsize=12)

# Mostrar la gráfica
plt.show()

Code
df_contract['EndDate'].value_counts()
EndDate
No                     5174
2019-11-01 00:00:00     485
2019-12-01 00:00:00     466
2020-01-01 00:00:00     460
2019-10-01 00:00:00     458
Name: count, dtype: int64
Code
gender=df_personal['gender'].value_counts().sort_values()
SeniorCitizen=df_personal['SeniorCitizen'].value_counts().sort_values()
Partner=df_personal['Partner'].value_counts().sort_values()
Dependents=df_personal['Dependents'].value_counts().sort_values()

  1. Podemos observar que hay un equilibrio entre los contratos a nombre de hombres y mujeres.

  2. En relación con los contratos de adultos mayores, notamos que son menos frecuentes.

  3. 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.

  4. 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 barras
bars=sns.barplot(x=InternetService.index, y=InternetService.values, ax=ax)

# Agregar el texto de la conclusión al lado de la gráfica
conclusion_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) * 100 for count in InternetService]
# Añadir etiquetas de porcentaje en cada barra
for bar, percentage in zip(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.7
text_y = InternetService.max() / 2

# Agregar el texto en la posición calculada
ax.text(text_x, text_y,conclusion_text, ha="center",va="center", fontsize=14)
# Ajustar el espaciado entre los subplots
plt.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 leyenda
legend_labels = [ 'Fribra Optica','DSL']
# Añadir la leyenda
ax.legend(legend_labels, loc='upper right', fontsize=12)

# Mostrar la gráfica
plt.show()

Code
# Crear un DataFrame con la estructura adecuada para graficar barras apiladas
services_counts = df_internet.iloc[:, 2:].apply(pd.Series.value_counts)


fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(15, 10))

for i, col in enumerate(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 subplots
plt.tight_layout()

# Mostrar la gráfica
plt.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 barras
bars=sns.barplot(x=MultipleLines.index, y=MultipleLines.values, ax=ax)

# Agregar el texto de la conclusión al lado de la gráfica
conclusion_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) * 100 for count in MultipleLines]
# Añadir etiquetas de porcentaje en cada barra
for bar, percentage in zip(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.7
text_y = MultipleLines.max() / 2

# Agregar el texto en la posición calculada
ax.text(text_x, text_y,conclusion_text, ha="center",va="center", fontsize=14)
# Ajustar el espaciado entre los subplots
plt.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áfica
plt.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 agrupadas
ax = cancel_counts.plot(kind='bar', stacked=False, color=['skyblue', 'salmon'], figsize=(8, 6))

# Agregar los valores de las barras
for 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áfico
plt.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áfica
plt.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 verticales
ax = cancel_metod['1'].plot(kind='bar', color='skyblue', figsize=(10, 6))

# Agregar texto al lado de las barras
for 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áfico
plt.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ón
conclusion_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ículas
sns.despine(left=True, bottom=True)
plt.grid(False)

# Mostrar la gráfica
plt.tight_layout()
plt.show()

Code
cancelacion_servicios=cancelacion_usuarios.merge(df_internet,on='customerID',how='outer')

cancelacion_servicios.head()
customerID BeginDate EndDate Type PaperlessBilling PaymentMethod MonthlyCharges TotalCharges cancel gender SeniorCitizen Partner Dependents InternetService OnlineSecurity OnlineBackup DeviceProtection TechSupport StreamingTV StreamingMovies
0 0002-ORFBO 2019-05-01 No One year Yes Mailed check 65.6 593.30 0 Female 0 Yes Yes DSL No Yes No Yes Yes No
1 0003-MKNFE 2019-05-01 No Month-to-month No Mailed check 59.9 542.40 0 Male 0 No No DSL No No No No No Yes
2 0004-TLHLJ 2019-09-01 2020-01-01 00:00:00 Month-to-month Yes Electronic check 73.9 280.85 1 Male 0 No No Fiber optic No No Yes No No No
3 0011-IGKFF 2018-12-01 2020-01-01 00:00:00 Month-to-month Yes Electronic check 98.0 1237.85 1 Male 1 Yes No Fiber optic No Yes Yes No Yes Yes
4 0013-EXCHZ 2019-09-01 2019-12-01 00:00:00 Month-to-month Yes Mailed check 83.9 267.40 1 Female 1 Yes No Fiber optic No No No Yes Yes No

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'].values
servicios = cancel_internet_service.index.tolist()

# Graficar el gráfico de pastel
plt.figure(figsize=(8, 6))
plt.pie(cancelaciones, labels=servicios, autopct='%1.1f%%', startangle=140, colors=['skyblue', 'salmon'])

# Personalización del gráfico
plt.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 cancelaciones
max_cancelaciones = max(cancelaciones)
indice_max = cancelaciones.tolist().index(max_cancelaciones)
servicio_max = servicios[indice_max]


# Agregar texto de conclusión
conclusion_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áfica
plt.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 barras
for 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áfico
plt.title('Cancelaciones por Mes')
plt.xlabel('Mes')
plt.ylabel('Número de Cancelaciones')
plt.xticks(rotation=45, ha='right')

# Agregar texto de conclusión
conclusion_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ículas
sns.despine(left=True, bottom=True)
plt.grid(False)

# Mostrar la gráfica
plt.tight_layout()
plt.show()

Creación de Modelo y Selección de características

Code
df_final_telecom=df_contract.merge(df_personal,how='outer').merge(df_internet,how='outer').merge(df_phone,how='outer')
df_final_telecom.head()
customerID BeginDate EndDate Type PaperlessBilling PaymentMethod MonthlyCharges TotalCharges cancel gender SeniorCitizen Partner Dependents InternetService OnlineSecurity OnlineBackup DeviceProtection TechSupport StreamingTV StreamingMovies MultipleLines
0 0002-ORFBO 2019-05-01 No One year Yes Mailed check 65.6 593.30 0 Female 0 Yes Yes DSL No Yes No Yes Yes No No
1 0003-MKNFE 2019-05-01 No Month-to-month No Mailed check 59.9 542.40 0 Male 0 No No DSL No No No No No Yes Yes
2 0004-TLHLJ 2019-09-01 2020-01-01 00:00:00 Month-to-month Yes Electronic check 73.9 280.85 1 Male 0 No No Fiber optic No No Yes No No No No
3 0011-IGKFF 2018-12-01 2020-01-01 00:00:00 Month-to-month Yes Electronic check 98.0 1237.85 1 Male 1 Yes No Fiber optic No Yes Yes No Yes Yes No
4 0013-EXCHZ 2019-09-01 2019-12-01 00:00:00 Month-to-month Yes Mailed check 83.9 267.40 1 Female 1 Yes No Fiber optic No No No Yes Yes No No

Tratamiento de Valores Nulos

Code
df_final_telecom.isna().sum()/df_final_telecom.shape[0]*100
customerID           0.000000
BeginDate            0.000000
EndDate              0.000000
Type                 0.000000
PaperlessBilling     0.000000
PaymentMethod        0.000000
MonthlyCharges       0.000000
TotalCharges         0.156183
cancel               0.000000
gender               0.000000
SeniorCitizen        0.000000
Partner              0.000000
Dependents           0.000000
InternetService     21.666903
OnlineSecurity      21.666903
OnlineBackup        21.666903
DeviceProtection    21.666903
TechSupport         21.666903
StreamingTV         21.666903
StreamingMovies     21.666903
MultipleLines        9.683374
dtype: float64

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

Code
nulos=df_final_telecom[df_final_telecom.isna().any(axis=1)]
nulos['cancel'].value_counts(normalize=True)*100
cancel
0    87.200362
1    12.799638
Name: proportion, dtype: float64

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.

Code
df_final_telecom['cancel']=df_final_telecom['cancel'].astype('int64')
df_final_telecom.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype         
---  ------            --------------  -----         
 0   customerID        7043 non-null   object        
 1   BeginDate         7043 non-null   datetime64[ns]
 2   EndDate           7043 non-null   object        
 3   Type              7043 non-null   object        
 4   PaperlessBilling  7043 non-null   object        
 5   PaymentMethod     7043 non-null   object        
 6   MonthlyCharges    7043 non-null   float64       
 7   TotalCharges      7032 non-null   float64       
 8   cancel            7043 non-null   int64         
 9   gender            7043 non-null   object        
 10  SeniorCitizen     7043 non-null   int64         
 11  Partner           7043 non-null   object        
 12  Dependents        7043 non-null   object        
 13  InternetService   5517 non-null   object        
 14  OnlineSecurity    5517 non-null   object        
 15  OnlineBackup      5517 non-null   object        
 16  DeviceProtection  5517 non-null   object        
 17  TechSupport       5517 non-null   object        
 18  StreamingTV       5517 non-null   object        
 19  StreamingMovies   5517 non-null   object        
 20  MultipleLines     6361 non-null   object        
dtypes: datetime64[ns](1), float64(2), int64(2), object(16)
memory usage: 1.1+ MB

Eliminar columnas que no nos proporcionan valor al Modelado

Code
df_final_telecom.drop(columns=['customerID','BeginDate','EndDate'],inplace=True)
  • Contamos con columnas de tipo fecha y una de ID

Utilizaremos Ordinal Encoder para la transformación de variables

Code
features = df_final_telecom.drop('cancel', axis=1) #x
target = df_final_telecom['cancel'] #y
X_train, X_test, y_train, y_test = train_test_split(features, target, test_size=0.20, random_state=12345)
Code
columns_object=df_final_telecom.select_dtypes(include='object').columns
enc = OrdinalEncoder()
X_train[columns_object] = enc.fit_transform(X_train[columns_object])
X_test[columns_object] = enc.transform(X_test[columns_object])
X_test.head()
Type PaperlessBilling PaymentMethod MonthlyCharges TotalCharges gender SeniorCitizen Partner Dependents InternetService OnlineSecurity OnlineBackup DeviceProtection TechSupport StreamingTV StreamingMovies MultipleLines
1128 2.0 0.0 1.0 19.90 1110.05 1.0 0 1.0 0.0 NaN NaN NaN NaN NaN NaN NaN 0.0
2875 1.0 1.0 2.0 103.80 3470.80 1.0 0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0
1783 0.0 1.0 1.0 30.50 208.70 1.0 0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 NaN
3804 0.0 0.0 2.0 94.95 178.10 1.0 0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0
6087 2.0 1.0 0.0 60.05 4176.70 1.0 0 0.0 0.0 0.0 1.0 1.0 0.0 1.0 1.0 1.0 NaN
Code
X_test.info()
<class 'pandas.core.frame.DataFrame'>
Index: 1409 entries, 1128 to 3409
Data columns (total 17 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   Type              1409 non-null   float64
 1   PaperlessBilling  1409 non-null   float64
 2   PaymentMethod     1409 non-null   float64
 3   MonthlyCharges    1409 non-null   float64
 4   TotalCharges      1409 non-null   float64
 5   gender            1409 non-null   float64
 6   SeniorCitizen     1409 non-null   int64  
 7   Partner           1409 non-null   float64
 8   Dependents        1409 non-null   float64
 9   InternetService   1110 non-null   float64
 10  OnlineSecurity    1110 non-null   float64
 11  OnlineBackup      1110 non-null   float64
 12  DeviceProtection  1110 non-null   float64
 13  TechSupport       1110 non-null   float64
 14  StreamingTV       1110 non-null   float64
 15  StreamingMovies   1110 non-null   float64
 16  MultipleLines     1279 non-null   float64
dtypes: float64(16), int64(1)
memory usage: 198.1 KB

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 nan
imputed_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.columns
imputed_X_valid.columns=X_test.columns
columns_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()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1409 entries, 0 to 1408
Data columns (total 17 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   Type              1409 non-null   float64
 1   PaperlessBilling  1409 non-null   float64
 2   PaymentMethod     1409 non-null   float64
 3   MonthlyCharges    1409 non-null   float64
 4   TotalCharges      1409 non-null   float64
 5   gender            1409 non-null   float64
 6   SeniorCitizen     1409 non-null   float64
 7   Partner           1409 non-null   float64
 8   Dependents        1409 non-null   float64
 9   InternetService   1409 non-null   float64
 10  OnlineSecurity    1409 non-null   Int64  
 11  OnlineBackup      1409 non-null   Int64  
 12  DeviceProtection  1409 non-null   Int64  
 13  TechSupport       1409 non-null   Int64  
 14  StreamingTV       1409 non-null   Int64  
 15  StreamingMovies   1409 non-null   Int64  
 16  MultipleLines     1409 non-null   Int64  
dtypes: Int64(7), float64(10)
memory usage: 196.9 KB

Ya no contamos con nulos

Data desbalanceada

Code
os_us = RandomOverSampler()
X_train_res, y_train_res = os_us.fit_resample(X_train, y_train)
resultado=pd.concat([X_train_res, y_train_res], axis=1)
resultado['cancel'].value_counts()
cancel
0    4125
1    4125
Name: count, dtype: int64

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

Code
# Funcion para probar los modelos y graficarla 

def evaluate_model(model, train_features, train_target, test_features, test_target):
    eval_stats = {}

    fig, axs = plt.subplots(1, 1, figsize=(10, 6))  # Solo un subplot para la curva ROC

    for type, features, target in (('train', train_features, train_target), ('test', test_features, test_target)):
        eval_stats[type] = {}

        pred_target = model.predict(features)
        pred_proba = model.predict_proba(features)[:, 1]

        # ROC
        fpr, tpr, _ = metrics.roc_curve(target, pred_proba)
        roc_auc = metrics.roc_auc_score(target, pred_proba)
        eval_stats[type]['ROC AUC'] = roc_auc

        if type == 'train':
            color = 'blue'
        else:
            color = 'green'

        # ROC
        ax = axs
        ax.plot(fpr, tpr, color=color, label=f'{type}, ROC AUC={roc_auc:.2f}')
        ax.plot([0, 1], [0, 1], color='grey', linestyle='--')
        ax.set_xlim([-0.02, 1.02])
        ax.set_ylim([-0.02, 1.02])
        ax.set_xlabel('FPR')
        ax.set_ylabel('TPR')
        ax.legend(loc='lower right')
        ax.set_title(f'Curva ROC')

        eval_stats[type]['Accuracy'] = metrics.accuracy_score(target, pred_target)

    df_eval_stats = pd.DataFrame(eval_stats)
    df_eval_stats = df_eval_stats.round(3)
    df_eval_stats = df_eval_stats.reindex(index=('Accuracy', 'ROC AUC'))

    print(df_eval_stats)

    plt.show()
    return

Esta función nos ayudara a calcular la curva roc y graficarla

Modelo 1 RandomForestClassifier

Code
dtc = RandomForestClassifier(criterion='entropy', max_depth=8,random_state=12345)
dtc.fit(X_train_res, y_train_res)

evaluate_model(dtc, X_train_res, y_train_res, X_test, y_test)
          train   test
Accuracy  0.829  0.771
ROC AUC   0.911  0.858

Modelo 2 LGBM Classifier

Code
lgbm = LGBMClassifier(random_state=12345,verbose=0)
lgbm.fit(X_train_res, y_train_res)
evaluate_model(lgbm, X_train_res, y_train_res, X_test, y_test)
          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 CatBoostClassifier
catboost = 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 balanceados
catboost.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.