- Exploré tres datasets y diagnostiqué nulos, sentinels y formatos de fecha.
- Imputé edades inválidas con la mediana y normalicé ciudades y fechas.
- Construí métricas de mensajes, llamadas y minutos por usuario mediante groupby y agg.
- Combiné el uso agregado con los atributos del cliente.
- Analicé distribuciones y outliers, preservando consumos posibles.
- Segmenté por uso y edad, y redacté recomendaciones para stakeholders.
- Documenté el proyecto en un notebook y lo compartí en GitHub.
ConnectaTel:
del consumo a la segmentación
Transformé 40.000 registros de llamadas y mensajes en 3.999 perfiles de clientes para identificar patrones de uso, detectar valores atípicos y orientar oportunidades comerciales en telecomunicaciones LATAM.
Resumen del proyecto
Un recorrido completo desde la revisión de los datos hasta la segmentación de clientes de ConnectaTel. Cada cifra conserva su universo de referencia: 4.000 clientes en users y 3.999 perfiles en user_profile.
Mi aporte fue convertir eventos operativos en una lectura clara del cliente: describir consumos típicos, separar anomalías de comportamientos reales y construir segmentos con reglas reproducibles.
Trabajé de forma programática: utilicé funciones para explorar variables, convertir fechas, crear histogramas y boxplots, calcular límites IQR y clasificar clientes.
| Característica | Básico | Premium |
|---|---|---|
| Clientes en users | 2.595 · 64,88 % | 1.405 · 35,13 % |
| Precio mensual | USD 12 | USD 25 |
| SMS incluidos / mes | 100 | 500 |
| Minutos incluidos / mes | 100 | 600 |
| GB incluidos / mes | 5 | 20 |
| Costo por GB extra | USD 1,20 | USD 1,00 |
| Costo por SMS extra | USD 0,08 | USD 0,05 |
| Costo por minuto extra | USD 0,10 | USD 0,07 |
Tarifas tomadas de plans.head() y participación calculada sobre users. La suma de porcentajes redondeados puede diferir de 100 %. El notebook no unió plans al perfil para calcular facturación ni analizó consumo de GB.
Proceso analítico, paso a paso
Los ocho pasos del notebook se mantienen en su orden original. Despliega los bloques técnicos para revisar el código y las salidas conservadas, sin ejecutar Python desde el navegador.
Paso 1. Carga y exploración
Importé Pandas, NumPy, Seaborn y Matplotlib. Cargué plans.csv, users_latam.csv y usage.csv; revisé primeras filas, dimensiones y tipos mediante head(), shape e info().
| Dataset | Filas × columnas | Contenido |
|---|---|---|
| plans | 2 × 8 | Tarifas, cupos y costos extra |
| users | 4.000 × 8 | Clientes, edad, ciudad, registro, plan y baja |
| usage | 40.000 × 6 | Eventos de llamadas y mensajes |
Ver código y salidas guardadas de este paso
Celda 3 · código original
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as pltSin salida textual guardada; las gráficas se presentan en la galería.
Celda 4 · código original
# cargar archivos
plans = pd.read_csv('/datasets/plans.csv')
users = pd.read_csv('/datasets/users_latam.csv')
usage = pd.read_csv('/datasets/usage.csv')Sin salida textual guardada; las gráficas se presentan en la galería.
Celda 5 · código original
plans.head(5)| plan_name | messages_included | gb_per_month | minutes_included | usd_monthly_pay | usd_per_gb | usd_per_message | usd_per_minute | |
|---|---|---|---|---|---|---|---|---|
| 0 | Basico | 100 | 5 | 100 | 12 | 1.2 | 0.08 | 0.10 |
| 1 | Premium | 500 | 20 | 600 | 25 | 1.0 | 0.05 | 0.07 |
Celda 6 · código original
users.head(5)| user_id | first_name | last_name | age | city | reg_date | plan | churn_date | |
|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | Carlos | Garcia | 38 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaN |
| 1 | 10001 | Mateo | Torres | 53 | ? | 2022-01-01 06:34:17.914478619 | Basico | NaN |
| 2 | 10002 | Sofia | Ramirez | 57 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaN |
| 3 | 10003 | Mateo | Ramirez | 69 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaN |
| 4 | 10004 | Mateo | Torres | 63 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaN |
Celda 7 · código original
usage.head(5)| id | user_id | type | date | duration | length | |
|---|---|---|---|---|---|---|
| 0 | 1 | 10332 | call | 2024-01-01 00:00:00.000000000 | 0.09 | NaN |
| 1 | 2 | 11458 | text | 2024-01-01 00:06:30.969774244 | NaN | 39.0 |
| 2 | 3 | 11777 | text | 2024-01-01 00:13:01.939548488 | NaN | 36.0 |
| 3 | 4 | 10682 | call | 2024-01-01 00:19:32.909322733 | 1.53 | NaN |
| 4 | 5 | 12742 | call | 2024-01-01 00:26:03.879096977 | 4.84 | NaN |
Celda 10 · código original
# revisar el número de filas y columnas de cada dataset
print("plans", plans.shape)
print("users", users.shape)
print("usage", usage.shape)plans (2, 8) users (4000, 8) usage (40000, 6)
Celda 11 · código original
# inspección de plans con .info()
plans.info()<class 'pandas.core.frame.DataFrame'> RangeIndex: 2 entries, 0 to 1 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 plan_name 2 non-null object 1 messages_included 2 non-null int64 2 gb_per_month 2 non-null int64 3 minutes_included 2 non-null int64 4 usd_monthly_pay 2 non-null int64 5 usd_per_gb 2 non-null float64 6 usd_per_message 2 non-null float64 7 usd_per_minute 2 non-null float64 dtypes: float64(3), int64(4), object(1) memory usage: 256.0+ bytes
Celda 12 · código original
# inspección de users con .info()
users.info()<class 'pandas.core.frame.DataFrame'> RangeIndex: 4000 entries, 0 to 3999 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 4000 non-null int64 1 first_name 4000 non-null object 2 last_name 4000 non-null object 3 age 4000 non-null int64 4 city 3531 non-null object 5 reg_date 4000 non-null object 6 plan 4000 non-null object 7 churn_date 466 non-null object dtypes: int64(2), object(6) memory usage: 250.1+ KB
Celda 13 · código original
# inspección de usage con .info()
usage.info()<class 'pandas.core.frame.DataFrame'> RangeIndex: 40000 entries, 0 to 39999 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 40000 non-null int64 1 user_id 40000 non-null int64 2 type 40000 non-null object 3 date 39950 non-null object 4 duration 17924 non-null float64 5 length 22104 non-null float64 dtypes: float64(2), int64(2), object(2) memory usage: 1.8+ MB
Paso 2. Diagnóstico de calidad
Medí nulos con isna().sum() y isna().mean(), exploré variables numéricas y categóricas con una función reutilizable e investigué patrones por tipo de servicio, plan y edad. Detecté -999 en edad y ? en ciudad.
| Campo | Hallazgo original | Proporción |
|---|---|---|
| city | 469 nulos + 96 sentinels “?” | 11,73 % nulos iniciales |
| churn_date | 3.534 sin fecha de baja | 88,35 % |
| usage.date | 50 fechas ausentes | 0,125 % |
| duration | 22.076 nulos | 55,19 % |
| length | 17.896 nulos | 44,74 % |
| reg_date | 40 fechas en 2026, posteriores al corte 2024 | 1,00 % |
La ausencia en duración/longitud depende principalmente de si el evento es SMS o llamada. No equiparé automáticamente esos nulos con información perdida.
Ver código y salidas guardadas de este paso
Celda 15 · código original
# cantidad de nulos para users
print("Cantidad de valores nulos") # Cantidad de valores nulos
print(users.isna().sum())
print()
print("Proporción de valores nulos")
print(users.isna().mean()) # Proporción de valores nulos
print()
print('Experimentación')
premium = users[users['plan'] == 'Premium']
print(users["city"].isna().groupby(users["plan"]).mean()) #queria rvisar si los nulos en las ciudades tienen relación con el plan escogido (MCAR)
print()
print(users['city'].isna().groupby(users['age']).mean()) #queria saber si hay relacion con algun grupo de edad en partiuclar (MCAR)
print()
print(premium['plan'].groupby(premium["city"]).count()/premium['plan'].groupby(premium["city"]).count().sum() * 100) #queria saber el pocentaje de personas con el plan premium por ciudad
print()
print(users['plan'].groupby(users["city"]).value_counts(normalize=True)) #queria ver la relación de los planes comprados por ciudad (se detecta sentinel ?)
print()
print(users['age'].groupby(users['city'].isna()).mean()) # queria agrupar por valores nulos y no nulos y que media de la edad estaba en cada uno (MAR)
print()
print(users["churn_date"].isna().groupby(users["plan"]).mean()) #queria revisar el porcentaje de fechas de baja tenian relación con el plan escogido, pero son similares (MCAR)
print()
print(users['city'].unique()) #queira confirmar los sentinelsCantidad de valores nulos
user_id 0
first_name 0
last_name 0
age 0
city 469
reg_date 0
plan 0
churn_date 3534
dtype: int64
Proporción de valores nulos
user_id 0.00000
first_name 0.00000
last_name 0.00000
age 0.00000
city 0.11725
reg_date 0.00000
plan 0.00000
churn_date 0.88350
dtype: float64
Experimentación
plan
Basico 0.115992
Premium 0.119573
Name: city, dtype: float64
age
-999 0.036364
18 0.123077
19 0.107692
20 0.140625
21 0.140351
...
75 0.129032
76 0.163934
77 0.086207
78 0.113924
79 0.084746
Name: city, Length: 63, dtype: float64
city
? 2.506063
Bogotá 23.120453
CDMX 20.695230
Cali 13.096200
GDL 12.287793
MTY 10.670978
Medellín 17.623282
Name: plan, dtype: float64
city plan
? Basico 0.677083
Premium 0.322917
Bogotá Basico 0.646040
Premium 0.353960
CDMX Basico 0.649315
Premium 0.350685
Cali Basico 0.617925
Premium 0.382075
GDL Basico 0.662222
Premium 0.337778
MTY Basico 0.675676
Premium 0.324324
Medellín Basico 0.646104
Premium 0.353896
Name: plan, dtype: float64
city
False 32.514868
True 42.961620
Name: age, dtype: float64
plan
Basico 0.885164
Premium 0.880427
Name: churn_date, dtype: float64
['Medellín' '?' 'CDMX' 'Bogotá' 'GDL' 'MTY' nan 'Cali']
Celda 16 · código original
# cantidad de nulos para usage
print("Cantidad de valores nulos") # Cantidad de valores nulos
print(usage.isna().sum())
print()
print("Proporción de valores nulos")
print(usage.isna().mean()) # Proporción de valores nulos
print()
print(usage['type'].unique())
print()
print('Relación de nulos en date con el type')
print(usage['date'].isna().groupby(usage['type']).mean())Cantidad de valores nulos id 0 user_id 0 type 0 date 50 duration 22076 length 17896 dtype: int64 Proporción de valores nulos id 0.00000 user_id 0.00000 type 0.00000 date 0.00125 duration 0.55190 length 0.44740 dtype: float64 ['call' 'text'] Relación de nulos en date con el type type call 0.001452 text 0.001086 Name: date, dtype: float64
Celda 19 · código original
# explorar columnas numéricas de users
""""
def explorar_num(df, columnas):
print(df.info())
for col in columnas:
print(),
print(f"{col}: Estadisticas descriptivas"),
print(df[col].describe()),
print()
return df
print(users['user_id'].dtype)
col_num_users = users[['user_id', 'age']]
explorar_num(users, col_num_users)
print('________________________________________________')
HASTA AQUÍ EXPERIMENTÉ CON CREAR FUNCIONES (ELIMINÉ EL CODIGO DE CALCULOS MAUALES POR ESPACIO, PERO SE ENTIENDE LA EXPERIMENTACION INICIAL"""
def explorar(df, columnas):
print(df.info())
for col in columnas:
if df[col].dtype == 'int64' or df[col].dtype == 'float64':
print(),
print(f"Estadisticas descriptivas de {col}"),
print("Mediana: ", df[col].median()),
print(df[col].describe()),
print('___________________________')
elif df[col].dtype == object:
print(),
print(f"Estadisticas Descriptivas de {col}"),
print(df[col].describe()),
print(),
print(f"Conteo de Nulos en {col}")
print(df[col].isna().value_counts()),
print(),
print(f"Porcentaje de Nulos en {col}"),
print(df[col].isna().value_counts(normalize=True)),
print(),
print(f"Participación Absoluta de cada categoría en {col}"),
print(df[col].value_counts()),
print(),
print(f"Participación Porcentual de cada categoría en {col}"),
print(df[col].value_counts(normalize=True))
print("________________________________________")
else:
return print('no concuerdan los tipos de datos')
col_num_users = users[['user_id', 'age']] #IGUAL SE DEFINEN QUE COLUMNAS TIENEN SENTIDO DE EXPLORAR
explorar(users, col_num_users)<class 'pandas.core.frame.DataFrame'> RangeIndex: 4000 entries, 0 to 3999 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 4000 non-null int64 1 first_name 4000 non-null object 2 last_name 4000 non-null object 3 age 4000 non-null float64 4 city 3435 non-null object 5 reg_date 3960 non-null datetime64[ns] 6 plan 4000 non-null object 7 churn_date 466 non-null datetime64[ns] dtypes: datetime64[ns](2), float64(1), int64(1), object(4) memory usage: 250.1+ KB None Estadisticas descriptivas de user_id Mediana: 11999.5 count 4000.000000 mean 11999.500000 std 1154.844867 min 10000.000000 25% 10999.750000 50% 11999.500000 75% 12999.250000 max 13999.000000 Name: user_id, dtype: float64 ___________________________ Estadisticas descriptivas de age Mediana: 48.0 count 4000.000000 mean 48.136000 std 17.689919 min 18.000000 25% 33.000000 50% 48.000000 75% 63.000000 max 79.000000 Name: age, dtype: float64 ___________________________
Celda 21 · código original
# explorar columnas numéricas de usage
""""
col_num_usage = usage[['id', 'user_id', 'duration', 'length']]
explorar_num(usage, col_num_usage)
print('___________________________________________')
HASTA AQUÍ EXPERIMENTÉ CON CREAR FUNCIONES (ELIMINÉ EL CODIGO DE CALCULOS MAUALES POR ESPACIO, PERO SE ENTIENDE LA EXPERIMENTACION INICIAL"""
col_num_usage = usage[['id', 'user_id', 'duration', 'length']] #IGUAL SE DEFINEN QUE COLUMNAS TIENEN SENTIDO DE EXPLORAR
explorar(usage, col_num_usage)<class 'pandas.core.frame.DataFrame'> RangeIndex: 40000 entries, 0 to 39999 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 40000 non-null int64 1 user_id 40000 non-null int64 2 type 40000 non-null object 3 date 39950 non-null object 4 duration 17924 non-null float64 5 length 22104 non-null float64 dtypes: float64(2), int64(2), object(2) memory usage: 1.8+ MB None Estadisticas descriptivas de id count 40000.00000 mean 20000.50000 std 11547.14972 min 1.00000 25% 10000.75000 50% 20000.50000 75% 30000.25000 max 40000.00000 Name: id, dtype: float64 Estadisticas descriptivas de user_id count 40000.000000 mean 12002.405975 std 1157.279564 min 10000.000000 25% 10996.000000 50% 12013.000000 75% 13005.000000 max 13999.000000 Name: user_id, dtype: float64 Estadisticas descriptivas de duration count 17924.000000 mean 5.202237 std 6.842701 min 0.000000 25% 1.437500 50% 3.500000 75% 6.990000 max 120.000000 Name: duration, dtype: float64 Estadisticas descriptivas de length count 22104.000000 mean 52.127398 std 56.611183 min 0.000000 25% 37.000000 50% 50.000000 75% 64.000000 max 1490.000000 Name: length, dtype: float64
Celda 23 · código original
# explorar columnas categóricas de users
""""
col_cat_users = users[['city', 'plan']]
def explorar_cat(df, columnas):
print(df.info())
for col in columnas:
print(),
print(f"Estadisticas Descriptivas de {col}"),
print(df[col].describe()),
print(),
print(f"Conteo de Nulos en {col}")
print(df[col].isna().value_counts()),
print(),
print(f"Porcentaje de Nulos en {col}"),
print(df[col].isna().value_counts(normalize=True)),
print(),
print(f"Participación Absoluta de cada categoría en {col}"),
print(df[col].value_counts()),
print(),
print(f"Participación Porcentual de cada categoría en {col}"),
print(df[col].value_counts(normalize=True))
print('________________________________________')
return df
explorar_cat(users, col_cat_users)
print('_____________________________')
users.info()
col_cat_users = users[['city', 'plan']]
print()
print('city')
print(col_cat_users['city'].describe())
print()
print('Proporción de nulos')
print(col_cat_users["city"].isna().value_counts(normalize=True))
print()
print('valores categoricos')
print(col_cat_users['city'].value_counts())
print()
print('plan: Estadisticas Descriptivas')
print(col_cat_users['plan'].describe())
print()
print(col_cat_users["plan"].isna().value_counts(normalize=True))
print()
print('Participación Absoluta')
print(col_cat_users['plan'].value_counts())
print()
print('Participación Porcentual')
print(col_cat_users['plan'].value_counts(normalize=True))
HASTA AQUÍ EXPERIMENTÉ CON CREAR FUNCIONES (CONSERVANDO EL CODIGO DE CALCULOS MAUALES, SE ENTIENDE LA EXPERIMENTACION INICIAL)"""
col_cat_users = users[['city', 'plan']] #IGUAL SE DEFINEN QUE COLUMNAS TIENEN SENTIDO DE EXPLORAR
explorar(users, col_cat_users)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 4000 entries, 0 to 3999 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 4000 non-null int64 1 first_name 4000 non-null object 2 last_name 4000 non-null object 3 age 4000 non-null int64 4 city 3531 non-null object 5 reg_date 4000 non-null object 6 plan 4000 non-null object 7 churn_date 466 non-null object dtypes: int64(2), object(6) memory usage: 250.1+ KB None Estadisticas Descriptivas de city count 3531 unique 7 top Bogotá freq 808 Name: city, dtype: object Conteo de Nulos en city False 3531 True 469 Name: city, dtype: int64 Porcentaje de Nulos en city False 0.88275 True 0.11725 Name: city, dtype: float64 Participación Absoluta de cada categoría en city Bogotá 808 CDMX 730 Medellín 616 GDL 450 Cali 424 MTY 407 ? 96 Name: city, dtype: int64 Participación Porcentual de cada categoría en city Bogotá 0.228830 CDMX 0.206740 Medellín 0.174455 GDL 0.127443 Cali 0.120079 MTY 0.115265 ? 0.027188 Name: city, dtype: float64 ________________________________________ Estadisticas Descriptivas de plan count 4000 unique 2 top Basico freq 2595 Name: plan, dtype: object Conteo de Nulos en plan False 4000 Name: plan, dtype: int64 Porcentaje de Nulos en plan False 1.0 Name: plan, dtype: float64 Participación Absoluta de cada categoría en plan Basico 2595 Premium 1405 Name: plan, dtype: int64 Participación Porcentual de cada categoría en plan Basico 0.64875 Premium 0.35125 Name: plan, dtype: float64 ________________________________________
Celda 25 · código original
# explorar columna categórica de usage
""""
usage['type'] # completa el código
usage.info()
col_cat_usage = usage['type']
print()
print('type')
print(col_cat_usage.describe())
print()
print('Proporción de nulos')
print(col_cat_usage.isna().value_counts(normalize=True))
print()
print('valores categoricos')
print(col_cat_usage.value_counts())
print()
print('Participación')
print(col_cat_usage.value_counts(normalize=True))
HASTA AQUÍ EXPERIMENTÉ CON CREAR FUNCIONES (CONSERVANDO EL CODIGO DE CALCULOS MAUALES, SE ENTIENDE LA EXPERIMENTACION INICIAL)"""
col_cat_usage = usage[['type']] #puse doble corchete para que siguiera identificando como dataframe
explorar(usage, col_cat_usage)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 40000 entries, 0 to 39999 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 40000 non-null int64 1 user_id 40000 non-null int64 2 type 40000 non-null object 3 date 39950 non-null object 4 duration 17924 non-null float64 5 length 22104 non-null float64 dtypes: float64(2), int64(2), object(2) memory usage: 1.8+ MB None Estadisticas Descriptivas de type count 40000 unique 2 top text freq 22092 Name: type, dtype: object Conteo de Nulos en type False 40000 Name: type, dtype: int64 Porcentaje de Nulos en type False 1.0 Name: type, dtype: float64 Participación Absoluta de cada categoría en type text 22092 call 17908 Name: type, dtype: int64 Participación Porcentual de cada categoría en type text 0.5523 call 0.4477 Name: type, dtype: float64 ________________________________________
Celda 29 · código original
# Convertir a fecha la columna `reg_date` de users
def convertir_columnas_numericas(df, columnas): # se pasó la columna churn_date de string a numerico y de numerico a datetime
for col in columnas:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df
def convertir_columnas_fechas(df, columnas):
for col in columnas:
df[col] = pd.to_datetime(df[col], errors="coerce")
return df
def convertir_columnas_fechas_nanos(df, columnas): #me equivoqué al plantear la columna churn_date como fecha escrita en string
for col in columnas:
df[col] = pd.to_datetime(df[col], unit='ns', errors="coerce") # La columna estaba escrita en ns y debia ser tranformada
return df
columnas_fechas_users = users[['reg_date']]
columnas_fechas_nano_users = users[['churn_date']]
convertir_columnas_fechas(users, columnas_fechas_users)
users.info()
print('____________________')
# si se presenta error en este codigo opcional numeralos con (#) y lugo ejecuta en orden 1 por 1 (desactivando la función anteiror tambien con #): str.replace -> convertir_columnas_numericas -> convertir_columnas_fechas_nanos
users['churn_date'] = users['churn_date'].str.replace(",", ".") #USE ESTO PARA MODIFICAR CHURN_DATE QUE ESTABA EN TEXTO CON COMAS O PUNTOS DECIMALES
convertir_columnas_numericas(users, columnas_fechas_nano_users)
convertir_columnas_fechas_nanos(users, columnas_fechas_nano_users)
print(users[users['churn_date'].notna()].head(10)) #queria saber como estaba siendo representada la columna churn_dte y es en nanosegundo por lo que no se podía convertir anteriormente con pd.datetiem al igual que la otra columna
print(users['churn_date'].head(10))
print(users['reg_date'].head(10))
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4000 entries, 0 to 3999
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 user_id 4000 non-null int64
1 first_name 4000 non-null object
2 last_name 4000 non-null object
3 age 4000 non-null float64
4 city 3435 non-null object
5 reg_date 4000 non-null datetime64[ns]
6 plan 4000 non-null object
7 churn_date 466 non-null datetime64[ns]
dtypes: datetime64[ns](2), float64(1), int64(1), object(4)
memory usage: 250.1+ KB
____________________
user_id first_name last_name age city \
32 10032 Carlos Lopez 44.0 MTY
38 10038 Carlos Torres 36.0 Medellín
42 10042 Mateo Gomez 56.0 Cali
44 10044 Mateo Garcia 58.0 Bogotá
47 10047 Carlos Ramirez 54.0 Bogotá
54 10054 Luis Garcia 43.0 Medellín
56 10056 Luis Garcia 29.0 CDMX
69 10069 Mariana Ramirez 45.0 GDL
73 10073 Carlos Torres 50.0 NaN
76 10076 Luis Torres 67.0 MTY
reg_date plan churn_date
32 2022-01-09 18:17:33.263315829 Premium 2024-06-13 23:06:40
38 2022-01-11 09:43:20.750187547 Premium 2024-09-12 00:13:20
42 2022-01-12 12:00:32.408102025 Basico 2024-07-22 23:13:20
44 2022-01-13 01:09:08.237059264 Basico 2024-11-10 00:53:20
47 2022-01-13 20:52:01.980495123 Premium 2024-10-08 23:26:40
54 2022-01-15 18:52:07.381845461 Premium 2024-08-13 00:46:40
56 2026-05-10 00:00:00.000000000 Basico 2024-07-25 01:13:20
69 2022-01-19 21:26:36.099024756 Basico 2024-08-02 23:06:40
73 2022-01-20 23:43:47.756939235 Basico 2024-10-05 23:13:20
76 2022-01-21 19:26:41.500375094 Basico 2024-12-13 00:33:20
0 2022-01-01 00:00:00.000000000
1 2022-01-01 06:34:17.914478619
2 2022-01-01 13:08:35.828957239
3 2022-01-01 19:42:53.743435858
4 2022-01-02 02:17:11.657914478
5 2022-01-02 08:51:29.572393098
6 2022-01-02 15:25:47.486871717
7 2022-01-02 22:00:05.401350337
8 2022-01-03 04:34:23.315828957
9 2022-01-03 11:08:41.230307576
Name: reg_date, dtype: datetime64[ns]
Celda 30 · código original
# Convertir a fecha la columna `date` de usage
columnas_fechas_usage = usage[['date']] # se deja como dataframe para que la funcion la lea sin inconvenientes
convertir_columnas_fechas(usage, columnas_fechas_usage)
#usage['date'] = # completa el código
| id | user_id | type | date | duration | length | |
|---|---|---|---|---|---|---|
| 0 | 1 | 10332 | call | 2024-01-01 00:00:00.000000000 | 0.09 | NaN |
| 1 | 2 | 11458 | text | 2024-01-01 00:06:30.969774244 | NaN | 39.0 |
| 2 | 3 | 11777 | text | 2024-01-01 00:13:01.939548488 | NaN | 36.0 |
| 3 | 4 | 10682 | call | 2024-01-01 00:19:32.909322733 | 1.53 | NaN |
| 4 | 5 | 12742 | call | 2024-01-01 00:26:03.879096977 | 4.84 | NaN |
| ... | ... | ... | ... | ... | ... | ... |
| 39995 | 39996 | 13497 | call | 2024-06-29 23:33:56.120903022 | 5.75 | NaN |
| 39996 | 39997 | 10941 | call | 2024-06-29 23:40:27.090677266 | 3.06 | NaN |
| 39997 | 39998 | 13038 | call | 2024-06-29 23:46:58.060451510 | 8.74 | NaN |
| 39998 | 39999 | 10863 | text | 2024-06-29 23:53:29.030225754 | NaN | 43.0 |
| 39999 | 40000 | 10759 | call | 2024-06-30 00:00:00.000000000 | 1.32 | NaN |
40000 rows × 6 columns
Celda 31 · código original
# Revisar los años presentes en `reg_date` de users
#print(users['reg_date'].dt.year, users['reg_date'].dt.month, users['reg_date'].dt.day)
print(users[users['reg_date'] > pd.Timestamp('2024-12-31')]['reg_date'].dt.year.value_counts())2026 40 Name: reg_date, dtype: int64
Celda 33 · código original
# Revisar los años presentes en `date` de usage
print(usage[usage['date'] < pd.Timestamp('2024-07-01')]['date'].dt.year.sort_values())
#print(usage[usage['date'] > pd.Timestamp('2024-12-31')]['date'].dt.month.sort_values())
#print(usage[usage['date'] > pd.Timestamp('2024-12-31')]['date'].dt.year.sort_values())
# print(users[users['churn_date'].notna()]['churn_date'].dt.year.sort_values(), users[users['churn_date'].notna()]['churn_date'].dt.month.sort_values(), users[users['churn_date'].notna()]['churn_date'].dt.day.sort_values())
# churn_day es necesario aplicar filtro y marca de ascendecia para que quede igual0 2024
26664 2024
26665 2024
26666 2024
26667 2024
...
13332 2024
13333 2024
13334 2024
13327 2024
39999 2024
Name: date, Length: 39950, dtype: int64
Paso 3. Limpieza y fechas
Convertí las edades no positivas a NaN y las imputé con la mediana de 48 años. Reemplacé ? por nulos en ciudad, sin inventar ubicaciones. Convertí reg_date y date a datetime; normalicé comas en churn_date, convertí a número y luego a fecha usando nanosegundos.
Marqué como NaT las 40 fechas de registro posteriores al 31 de diciembre de 2024. Conservé los usuarios: la limpieza no eliminó esas filas. Tras normalizar ciudad, quedaron 565 nulos (14,13 %) y 3.435 ciudades conocidas.
churn_date se conservaron por su significado de negocio: no hay una baja registrada. En duration y length mantuve la estructura por tipo de evento.Ver código y salidas guardadas de este paso
Celda 29 · código original
# Convertir a fecha la columna `reg_date` de users
def convertir_columnas_numericas(df, columnas): # se pasó la columna churn_date de string a numerico y de numerico a datetime
for col in columnas:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df
def convertir_columnas_fechas(df, columnas):
for col in columnas:
df[col] = pd.to_datetime(df[col], errors="coerce")
return df
def convertir_columnas_fechas_nanos(df, columnas): #me equivoqué al plantear la columna churn_date como fecha escrita en string
for col in columnas:
df[col] = pd.to_datetime(df[col], unit='ns', errors="coerce") # La columna estaba escrita en ns y debia ser tranformada
return df
columnas_fechas_users = users[['reg_date']]
columnas_fechas_nano_users = users[['churn_date']]
convertir_columnas_fechas(users, columnas_fechas_users)
users.info()
print('____________________')
# si se presenta error en este codigo opcional numeralos con (#) y lugo ejecuta en orden 1 por 1 (desactivando la función anteiror tambien con #): str.replace -> convertir_columnas_numericas -> convertir_columnas_fechas_nanos
users['churn_date'] = users['churn_date'].str.replace(",", ".") #USE ESTO PARA MODIFICAR CHURN_DATE QUE ESTABA EN TEXTO CON COMAS O PUNTOS DECIMALES
convertir_columnas_numericas(users, columnas_fechas_nano_users)
convertir_columnas_fechas_nanos(users, columnas_fechas_nano_users)
print(users[users['churn_date'].notna()].head(10)) #queria saber como estaba siendo representada la columna churn_dte y es en nanosegundo por lo que no se podía convertir anteriormente con pd.datetiem al igual que la otra columna
print(users['churn_date'].head(10))
print(users['reg_date'].head(10))
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4000 entries, 0 to 3999
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 user_id 4000 non-null int64
1 first_name 4000 non-null object
2 last_name 4000 non-null object
3 age 4000 non-null float64
4 city 3435 non-null object
5 reg_date 4000 non-null datetime64[ns]
6 plan 4000 non-null object
7 churn_date 466 non-null datetime64[ns]
dtypes: datetime64[ns](2), float64(1), int64(1), object(4)
memory usage: 250.1+ KB
____________________
user_id first_name last_name age city \
32 10032 Carlos Lopez 44.0 MTY
38 10038 Carlos Torres 36.0 Medellín
42 10042 Mateo Gomez 56.0 Cali
44 10044 Mateo Garcia 58.0 Bogotá
47 10047 Carlos Ramirez 54.0 Bogotá
54 10054 Luis Garcia 43.0 Medellín
56 10056 Luis Garcia 29.0 CDMX
69 10069 Mariana Ramirez 45.0 GDL
73 10073 Carlos Torres 50.0 NaN
76 10076 Luis Torres 67.0 MTY
reg_date plan churn_date
32 2022-01-09 18:17:33.263315829 Premium 2024-06-13 23:06:40
38 2022-01-11 09:43:20.750187547 Premium 2024-09-12 00:13:20
42 2022-01-12 12:00:32.408102025 Basico 2024-07-22 23:13:20
44 2022-01-13 01:09:08.237059264 Basico 2024-11-10 00:53:20
47 2022-01-13 20:52:01.980495123 Premium 2024-10-08 23:26:40
54 2022-01-15 18:52:07.381845461 Premium 2024-08-13 00:46:40
56 2026-05-10 00:00:00.000000000 Basico 2024-07-25 01:13:20
69 2022-01-19 21:26:36.099024756 Basico 2024-08-02 23:06:40
73 2022-01-20 23:43:47.756939235 Basico 2024-10-05 23:13:20
76 2022-01-21 19:26:41.500375094 Basico 2024-12-13 00:33:20
0 2022-01-01 00:00:00.000000000
1 2022-01-01 06:34:17.914478619
2 2022-01-01 13:08:35.828957239
3 2022-01-01 19:42:53.743435858
4 2022-01-02 02:17:11.657914478
5 2022-01-02 08:51:29.572393098
6 2022-01-02 15:25:47.486871717
7 2022-01-02 22:00:05.401350337
8 2022-01-03 04:34:23.315828957
9 2022-01-03 11:08:41.230307576
Name: reg_date, dtype: datetime64[ns]
Celda 30 · código original
# Convertir a fecha la columna `date` de usage
columnas_fechas_usage = usage[['date']] # se deja como dataframe para que la funcion la lea sin inconvenientes
convertir_columnas_fechas(usage, columnas_fechas_usage)
#usage['date'] = # completa el código
| id | user_id | type | date | duration | length | |
|---|---|---|---|---|---|---|
| 0 | 1 | 10332 | call | 2024-01-01 00:00:00.000000000 | 0.09 | NaN |
| 1 | 2 | 11458 | text | 2024-01-01 00:06:30.969774244 | NaN | 39.0 |
| 2 | 3 | 11777 | text | 2024-01-01 00:13:01.939548488 | NaN | 36.0 |
| 3 | 4 | 10682 | call | 2024-01-01 00:19:32.909322733 | 1.53 | NaN |
| 4 | 5 | 12742 | call | 2024-01-01 00:26:03.879096977 | 4.84 | NaN |
| ... | ... | ... | ... | ... | ... | ... |
| 39995 | 39996 | 13497 | call | 2024-06-29 23:33:56.120903022 | 5.75 | NaN |
| 39996 | 39997 | 10941 | call | 2024-06-29 23:40:27.090677266 | 3.06 | NaN |
| 39997 | 39998 | 13038 | call | 2024-06-29 23:46:58.060451510 | 8.74 | NaN |
| 39998 | 39999 | 10863 | text | 2024-06-29 23:53:29.030225754 | NaN | 43.0 |
| 39999 | 40000 | 10759 | call | 2024-06-30 00:00:00.000000000 | 1.32 | NaN |
40000 rows × 6 columns
Celda 37 · código original
# Reemplazar -999 por la mediana de age
users.info()
users.loc[users['age'] <= 0, 'age'] = np.nan #codigo copiado del sprint
age_mediana = users['age'].median()
users['age'] = users['age'].fillna(age_mediana)
#Verificar cambios
users['age'].describe()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 4000 entries, 0 to 3999 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 4000 non-null int64 1 first_name 4000 non-null object 2 last_name 4000 non-null object 3 age 4000 non-null int64 4 city 3531 non-null object 5 reg_date 4000 non-null datetime64[ns] 6 plan 4000 non-null object 7 churn_date 466 non-null object dtypes: datetime64[ns](1), int64(2), object(5) memory usage: 250.1+ KB
count 4000.000000 mean 48.136000 std 17.689919 min 18.000000 25% 33.000000 50% 48.000000 75% 63.000000 max 79.000000 Name: age, dtype: float64
Celda 38 · código original
# Reemplazar ? por NA en city
users.loc[users['city'] == '?', 'city'] = np.nan #codigo copiado del sprint
# Verificar cambios
users['city'].describe()
users['city'].value_counts()
Bogotá 808 CDMX 730 Medellín 616 GDL 450 Cali 424 MTY 407 Name: city, dtype: int64
Celda 39 · código original
# Marcar fechas futuras como NA para reg_date
#POR LO ENTENDIDO POR LA IA, SOLO HAY QUE CORTAR LAS 40 FECHAS DESPUES DEL 31 DE DICIEMBRE DE 2024
users.loc[users['reg_date'] > pd.Timestamp('2024-12-31'), 'reg_date'] = pd.NaT
# Verificar cambios
print(users[users['reg_date'] < pd.Timestamp('2024-12-31')]['reg_date'].dt.year.describe()) # o .value_counts()
users.info()count 3959.000000 mean 2023.003789 std 0.817158 min 2022.000000 25% 2022.000000 50% 2023.000000 75% 2024.000000 max 2024.000000 Name: reg_date, dtype: float64 <class 'pandas.core.frame.DataFrame'> RangeIndex: 4000 entries, 0 to 3999 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 4000 non-null int64 1 first_name 4000 non-null object 2 last_name 4000 non-null object 3 age 4000 non-null float64 4 city 3435 non-null object 5 reg_date 3960 non-null datetime64[ns] 6 plan 4000 non-null object 7 churn_date 466 non-null datetime64[ns] dtypes: datetime64[ns](2), float64(1), int64(1), object(4) memory usage: 250.1+ KB
Celda 41 · código original
# Verificación MAR en usage (Missing At Random) para duration
usage.info()
print(usage['duration'].isna().groupby(usage['type']).mean())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 40000 entries, 0 to 39999 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 40000 non-null int64 1 user_id 40000 non-null int64 2 type 40000 non-null object 3 date 39950 non-null datetime64[ns] 4 duration 17924 non-null float64 5 length 22104 non-null float64 dtypes: datetime64[ns](1), float64(2), int64(2), object(1) memory usage: 1.8+ MB type call 0.000000 text 0.999276 Name: duration, dtype: float64
Celda 42 · código original
# Verificación MAR en usage (Missing At Random) para length
print(usage['length'].isna().groupby(usage['type']).mean())
type call 0.99933 text 0.00000 Name: length, dtype: float64
Paso 4. Perfil estadístico por usuario
Creé is_text e is_call, agrupé por user_id y sumé mensajes, llamadas y duración. Uní usage_agg con users mediante pd.merge(); sin especificar how, el código conserva las coincidencias.
| Métrica por perfil | Media | Mediana | Máximo |
|---|---|---|---|
| Mensajes | 5,52 | 5 | 17 |
| Llamadas | 4,48 | 4 | 15 |
| Minutos agregados | 23,32 | 19,78 | 155,69 |
Son acumulados del periodo disponible, no promedios mensuales. El perfil incluye 3.999 usuarios frente a 4.000 en la tabla maestra (99,975 %); no se construyó un perfil de cero consumo para el usuario sin coincidencia.
Ver código y salidas guardadas de este paso
Celda 45 · código original
# Columnas auxiliares
usage["is_text"] = (usage["type"] == "text").astype(int) #conocer el total de mensajes
usage["is_call"] = (usage["type"] == "call").astype(int) #conocer el total de llamadas
# Agrupar información por usuario
usage_agg = usage.groupby('user_id').agg(cant_mensajes = ('is_text', 'sum'), cant_llamadas = ('is_call', 'sum'),
cant_minutos_llamada = ('duration', 'sum')).reset_index()
# observar resultado
usage_agg.head(3)| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | |
|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 |
| 1 | 10001 | 5 | 10 | 33.18 |
| 2 | 10002 | 5 | 2 | 10.74 |
Celda 46 · código original
# Renombrar columnas
#usage_agg = usage_agg.rename(columns={'cant_mensajes': ' '}) NO LO HAGO PORQUE YA PUSE EL NOMBRE CORRECTO DESDE AGG. NO ENTIENDO ESTE PASO MUY BIEN
# observar resultado
usage_agg.head(3)| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | |
|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 |
| 1 | 10001 | 5 | 10 | 33.18 |
| 2 | 10002 | 5 | 2 | 10.74 |
Celda 47 · código original
# Combinar la tabla agregada con el dataset de usuarios
user_profile = pd.merge(usage_agg, users, on='user_id')
user_profile.head(5)
| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT |
Celda 49 · código original
# Resumen estadístico de las columnas numéricas
# col_num_users & col_num_usage
print(explorar(users, col_num_users))
print(explorar(usage, col_num_usage))
print(explorar(users, col_cat_users))<class 'pandas.core.frame.DataFrame'> RangeIndex: 4000 entries, 0 to 3999 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 4000 non-null int64 1 first_name 4000 non-null object 2 last_name 4000 non-null object 3 age 4000 non-null float64 4 city 3435 non-null object 5 reg_date 3960 non-null datetime64[ns] 6 plan 4000 non-null object 7 churn_date 466 non-null datetime64[ns] dtypes: datetime64[ns](2), float64(1), int64(1), object(4) memory usage: 250.1+ KB None Estadisticas descriptivas de user_id Mediana: 11999.5 count 4000.000000 mean 11999.500000 std 1154.844867 min 10000.000000 25% 10999.750000 50% 11999.500000 75% 12999.250000 max 13999.000000 Name: user_id, dtype: float64 ___________________________ Estadisticas descriptivas de age Mediana: 48.0 count 4000.000000 mean 48.136000 std 17.689919 min 18.000000 25% 33.000000 50% 48.000000 75% 63.000000 max 79.000000 Name: age, dtype: float64 ___________________________ None <class 'pandas.core.frame.DataFrame'> RangeIndex: 40000 entries, 0 to 39999 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 40000 non-null int64 1 user_id 40000 non-null int64 2 type 40000 non-null object 3 date 39950 non-null datetime64[ns] 4 duration 17924 non-null float64 5 length 22104 non-null float64 6 is_text 40000 non-null int64 7 is_call 40000 non-null int64 dtypes: datetime64[ns](1), float64(2), int64(4), object(1) memory usage: 2.4+ MB None Estadisticas descriptivas de id Mediana: 20000.5 count 40000.00000 mean 20000.50000 std 11547.14972 min 1.00000 25% 10000.75000 50% 20000.50000 75% 30000.25000 max 40000.00000 Name: id, dtype: float64 ___________________________ Estadisticas descriptivas de user_id Mediana: 12013.0 count 40000.000000 mean 12002.405975 std 1157.279564 min 10000.000000 25% 10996.000000 50% 12013.000000 75% 13005.000000 max 13999.000000 Name: user_id, dtype: float64 ___________________________ Estadisticas descriptivas de duration Mediana: 3.5 count 17924.000000 mean 5.202237 std 6.842701 min 0.000000 25% 1.437500 50% 3.500000 75% 6.990000 max 120.000000 Name: duration, dtype: float64 ___________________________ Estadisticas descriptivas de length Mediana: 50.0 count 22104.000000 mean 52.127398 std 56.611183 min 0.000000 25% 37.000000 50% 50.000000 75% 64.000000 max 1490.000000 Name: length, dtype: float64 ___________________________ None <class 'pandas.core.frame.DataFrame'> RangeIndex: 4000 entries, 0 to 3999 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 4000 non-null int64 1 first_name 4000 non-null object 2 last_name 4000 non-null object 3 age 4000 non-null float64 4 city 3435 non-null object 5 reg_date 3960 non-null datetime64[ns] 6 plan 4000 non-null object 7 churn_date 466 non-null datetime64[ns] dtypes: datetime64[ns](2), float64(1), int64(1), object(4) memory usage: 250.1+ KB None Estadisticas Descriptivas de city count 3435 unique 6 top Bogotá freq 808 Name: city, dtype: object Conteo de Nulos en city False 3435 True 565 Name: city, dtype: int64 Porcentaje de Nulos en city False 0.85875 True 0.14125 Name: city, dtype: float64 Participación Absoluta de cada categoría en city Bogotá 808 CDMX 730 Medellín 616 GDL 450 Cali 424 MTY 407 Name: city, dtype: int64 Participación Porcentual de cada categoría en city Bogotá 0.235226 CDMX 0.212518 Medellín 0.179330 GDL 0.131004 Cali 0.123435 MTY 0.118486 Name: city, dtype: float64 ________________________________________ Estadisticas Descriptivas de plan count 4000 unique 2 top Basico freq 2595 Name: plan, dtype: object Conteo de Nulos en plan False 4000 Name: plan, dtype: int64 Porcentaje de Nulos en plan False 1.0 Name: plan, dtype: float64 Participación Absoluta de cada categoría en plan Basico 2595 Premium 1405 Name: plan, dtype: int64 Participación Porcentual de cada categoría en plan Basico 0.64875 Premium 0.35125 Name: plan, dtype: float64 ________________________________________ None
Celda 50 · código original
# Distribución porcentual del tipo de plan (HECHO JUSTO AQUI ARRIBA) es...
print("Distribución porcentual del tipo de plan")
print(users['plan'].value_counts(normalize=True))Distribución porcentual del tipo de plan Basico 0.64875 Premium 0.35125 Name: plan, dtype: float64
Paso 5. Distribuciones y outliers
Construí cuatro histogramas con 10 intervalos, curva KDE y comparación por plan: edad, mensajes, llamadas y minutos. Añadí cuatro boxplots para evaluar valores atípicos.
Calculé IQR = Q3 − Q1 y los límites Q1 − 1,5 × IQR y Q3 + 1,5 × IQR. Conservé los extremos porque son consumos posibles, no errores demostrados; pueden ser útiles para segmentar usuarios intensivos.
| Variable | Q1 | Q3 | Límite superior | Máximo |
|---|---|---|---|---|
| Mensajes | 4 | 7 | 11,5 | 17 |
| Llamadas | 3 | 6 | 10,5 | 15 |
| Minutos | 11,12 | 31,415 | 61,8575 | 155,69 |
La edad no muestra outliers en el boxplot. Las variables de consumo presentan colas a la derecha. Un valor alto no demuestra por sí solo mayor rentabilidad.
Ver código y salidas guardadas de este paso
Celda 52 · código original
# Histograma para visualizar la edad (age)
def crear_hist(df, columnas, nombre_df): #colocar nombre distintivo
for col in columnas:
sns.histplot(data=df, x=col, bins=10, hue='plan', palette=['skyblue', 'green'], kde=True) #
plt.xlabel(f"{col}")
plt.ylabel(f"Cantidad de repeticiones de {col}")
plt.title(f"Histograma para visualizar '{col}' del df de {nombre_df}")
plt.show()
return df
crear_hist(user_profile, user_profile[['age']], "user_profile")| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3994 | 13995 | 6 | 3 | 21.24 | Ana | Lopez | 60.0 | Medellín | 2024-12-29 21:42:48.342085520 | Basico | NaT |
| 3995 | 13996 | 4 | 3 | 2.81 | Carlos | Ramirez | 24.0 | Medellín | 2024-12-30 04:17:06.256564144 | Premium | NaT |
| 3996 | 13997 | 5 | 3 | 11.34 | Ana | Lopez | 58.0 | Bogotá | 2024-12-30 10:51:24.171042768 | Premium | NaT |
| 3997 | 13998 | 6 | 6 | 22.95 | Mariana | Lopez | 57.0 | Bogotá | 2024-12-30 17:25:42.085521392 | Basico | NaT |
| 3998 | 13999 | 3 | 6 | 26.46 | Ana | Lopez | 20.0 | Bogotá | 2024-12-31 00:00:00.000000000 | Basico | NaT |
3999 rows × 11 columns
Celda 54 · código original
# Histograma para visualizar la cant_mensajes
crear_hist(user_profile, user_profile[['cant_mensajes']], "user_profile") # cant_llamadas cant_minutos_llamada| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3994 | 13995 | 6 | 3 | 21.24 | Ana | Lopez | 60.0 | Medellín | 2024-12-29 21:42:48.342085520 | Basico | NaT |
| 3995 | 13996 | 4 | 3 | 2.81 | Carlos | Ramirez | 24.0 | Medellín | 2024-12-30 04:17:06.256564144 | Premium | NaT |
| 3996 | 13997 | 5 | 3 | 11.34 | Ana | Lopez | 58.0 | Bogotá | 2024-12-30 10:51:24.171042768 | Premium | NaT |
| 3997 | 13998 | 6 | 6 | 22.95 | Mariana | Lopez | 57.0 | Bogotá | 2024-12-30 17:25:42.085521392 | Basico | NaT |
| 3998 | 13999 | 3 | 6 | 26.46 | Ana | Lopez | 20.0 | Bogotá | 2024-12-31 00:00:00.000000000 | Basico | NaT |
3999 rows × 11 columns
Celda 56 · código original
# Histograma para visualizar la cant_llamadas
crear_hist(user_profile, user_profile[['cant_llamadas']], "user_profile")| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3994 | 13995 | 6 | 3 | 21.24 | Ana | Lopez | 60.0 | Medellín | 2024-12-29 21:42:48.342085520 | Basico | NaT |
| 3995 | 13996 | 4 | 3 | 2.81 | Carlos | Ramirez | 24.0 | Medellín | 2024-12-30 04:17:06.256564144 | Premium | NaT |
| 3996 | 13997 | 5 | 3 | 11.34 | Ana | Lopez | 58.0 | Bogotá | 2024-12-30 10:51:24.171042768 | Premium | NaT |
| 3997 | 13998 | 6 | 6 | 22.95 | Mariana | Lopez | 57.0 | Bogotá | 2024-12-30 17:25:42.085521392 | Basico | NaT |
| 3998 | 13999 | 3 | 6 | 26.46 | Ana | Lopez | 20.0 | Bogotá | 2024-12-31 00:00:00.000000000 | Basico | NaT |
3999 rows × 11 columns
Celda 58 · código original
# Histograma para visualizar la cant_minutos_llamada
crear_hist(user_profile, user_profile[['cant_minutos_llamada']], "user_profile")| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3994 | 13995 | 6 | 3 | 21.24 | Ana | Lopez | 60.0 | Medellín | 2024-12-29 21:42:48.342085520 | Basico | NaT |
| 3995 | 13996 | 4 | 3 | 2.81 | Carlos | Ramirez | 24.0 | Medellín | 2024-12-30 04:17:06.256564144 | Premium | NaT |
| 3996 | 13997 | 5 | 3 | 11.34 | Ana | Lopez | 58.0 | Bogotá | 2024-12-30 10:51:24.171042768 | Premium | NaT |
| 3997 | 13998 | 6 | 6 | 22.95 | Mariana | Lopez | 57.0 | Bogotá | 2024-12-30 17:25:42.085521392 | Basico | NaT |
| 3998 | 13999 | 3 | 6 | 26.46 | Ana | Lopez | 20.0 | Bogotá | 2024-12-31 00:00:00.000000000 | Basico | NaT |
3999 rows × 11 columns
Celda 61 · código original
# Visualizando usando BoxPlot
columnas_numericas = ['age', 'cant_mensajes', 'cant_llamadas', 'cant_minutos_llamada']
def crear_boxplot(df, columnas):
for col in columnas:
sns.boxplot(data=df, x=col)
plt.title(f"Boxplot: {col}")
plt.show()
return df
crear_boxplot(user_profile, columnas_numericas)| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3994 | 13995 | 6 | 3 | 21.24 | Ana | Lopez | 60.0 | Medellín | 2024-12-29 21:42:48.342085520 | Basico | NaT |
| 3995 | 13996 | 4 | 3 | 2.81 | Carlos | Ramirez | 24.0 | Medellín | 2024-12-30 04:17:06.256564144 | Premium | NaT |
| 3996 | 13997 | 5 | 3 | 11.34 | Ana | Lopez | 58.0 | Bogotá | 2024-12-30 10:51:24.171042768 | Premium | NaT |
| 3997 | 13998 | 6 | 6 | 22.95 | Mariana | Lopez | 57.0 | Bogotá | 2024-12-30 17:25:42.085521392 | Basico | NaT |
| 3998 | 13999 | 3 | 6 | 26.46 | Ana | Lopez | 20.0 | Bogotá | 2024-12-31 00:00:00.000000000 | Basico | NaT |
3999 rows × 11 columns
Celda 63 · código original
# Calcular límites con el método IQR
columnas_limites = user_profile[["cant_mensajes", "cant_llamadas", "cant_minutos_llamada"]]
# Crea otro bucle para calcular los límites de esas columnas usando el método IQR y decide qué hacer con ellos
def metodo_IQR(df, columnas):
for col in columnas:
print(f"\n--- Columna: {col} ---")
#calcular Q1
Q1 = df[col].quantile(0.25)
print('Primer cuartil: ', Q1)
#calcular Q3
Q3 = df[col].quantile(0.75)
print('Tercer cuartil: ', Q3)
#calcular IQR
IQR = Q3 - Q1
print('IQR: ', IQR)
#calcular límite inferior
lower = Q1 - 1.5 * IQR
print('Límite inferior: ', lower)
#calcular límite superior
upper = Q3 + 1.5 * IQR
print('Límite superior: ', upper)
return df
metodo_IQR(user_profile, columnas_limites)
--- Columna: cant_mensajes --- Primer cuartil: 4.0 Tercer cuartil: 7.0 IQR: 3.0 Límite inferior: -0.5 Límite superior: 11.5 --- Columna: cant_llamadas --- Primer cuartil: 3.0 Tercer cuartil: 6.0 IQR: 3.0 Límite inferior: -1.5 Límite superior: 10.5 --- Columna: cant_minutos_llamada --- Primer cuartil: 11.12 Tercer cuartil: 31.415 IQR: 20.295 Límite inferior: -19.322500000000005 Límite superior: 61.8575
| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3994 | 13995 | 6 | 3 | 21.24 | Ana | Lopez | 60.0 | Medellín | 2024-12-29 21:42:48.342085520 | Basico | NaT |
| 3995 | 13996 | 4 | 3 | 2.81 | Carlos | Ramirez | 24.0 | Medellín | 2024-12-30 04:17:06.256564144 | Premium | NaT |
| 3996 | 13997 | 5 | 3 | 11.34 | Ana | Lopez | 58.0 | Bogotá | 2024-12-30 10:51:24.171042768 | Premium | NaT |
| 3997 | 13998 | 6 | 6 | 22.95 | Mariana | Lopez | 57.0 | Bogotá | 2024-12-30 17:25:42.085521392 | Basico | NaT |
| 3998 | 13999 | 3 | 6 | 26.46 | Ana | Lopez | 20.0 | Bogotá | 2024-12-31 00:00:00.000000000 | Basico | NaT |
3999 rows × 11 columns
Celda 64 · código original
# Revisa los limites superiores y el max, para tomar la decisión de mantener los outliers o no
columnas_limites.describe()| cant_mensajes | cant_llamadas | cant_minutos_llamada | |
|---|---|---|---|
| count | 3999.000000 | 3999.000000 | 3999.000000 |
| mean | 5.524381 | 4.478120 | 23.317054 |
| std | 2.358416 | 2.144238 | 18.168095 |
| min | 0.000000 | 0.000000 | 0.000000 |
| 25% | 4.000000 | 3.000000 | 11.120000 |
| 50% | 5.000000 | 4.000000 | 19.780000 |
| 75% | 7.000000 | 6.000000 | 31.415000 |
| max | 17.000000 | 15.000000 | 155.690000 |
Paso 6. Segmentación de clientes
Clasifiqué cada perfil con funciones, condicionales y apply(axis=1). Para uso: bajo si llamadas y mensajes son ambos < 5; medio si no es bajo y ambos son < 10; alto en los demás casos (al menos uno ≥ 10). Los faltantes se etiquetan “Error en Datos”.
Para edad: joven < 30, adulto de 30 a 59 y adulto mayor ≥ 60. Verifiqué los conteos con value_counts() y generé dos gráficos con countplot().
| Segmento de uso | Usuarios | % de 3.999 |
|---|---|---|
| Uso medio | 2.943 | 73,59 % |
| Bajo uso | 778 | 19,45 % |
| Alto uso | 278 | 6,95 % |
| Segmento de edad | Usuarios | % de 3.999 |
|---|---|---|
| Adulto | 2.017 | 50,44 % |
| Adulto Mayor | 1.222 | 30,56 % |
| Joven | 760 | 19,00 % |
Ver código y salidas guardadas de este paso
Celda 67 · código original
# Crear columna grupo_uso
# Función para clasificar clientes
def classify_segment(row):
llamadas = row['cant_llamadas']
mensajes = row['cant_mensajes']
# Manejo de valores nulos/faltantes
if pd.isna(llamadas) or pd.isna(mensajes):
return "Error en Datos"
# Segmentación
if llamadas < 5 and mensajes < 5:
return "Bajo uso"
elif llamadas < 10 and mensajes < 10:
return 'Uso medio'
else:
return 'Alto uso'
user_profile["grupo_uso"] = user_profile.apply(classify_segment, axis=1)
user_profile['grupo_uso'].value_counts()Uso medio 2943 Bajo uso 778 Alto uso 278 Name: grupo_uso, dtype: int64
Celda 68 · código original
# verificar cambios
user_profile.head()| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | grupo_uso | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT | Uso medio |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT | Alto uso |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT | Uso medio |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT | Alto uso |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT | Bajo uso |
Celda 70 · código original
# Crear columna grupo_edad
# Función para clasificar clientes
def classify_segment(row):
edad = row['age']
# Manejo de valores nulos/faltantes
if pd.isna(edad):
return "Error en Datos"
# Segmentación
if edad < 30:
return "Joven"
elif edad < 60:
return 'Adulto'
else:
return 'Adulto Mayor'
user_profile["grupo_edad"] = user_profile.apply(classify_segment, axis=1)
user_profile['grupo_edad'].value_counts()Adulto 2017 Adulto Mayor 1222 Joven 760 Name: grupo_edad, dtype: int64
Celda 71 · código original
# verificar cambios
user_profile.head()| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | grupo_uso | grupo_edad | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT | Uso medio | Adulto |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT | Alto uso | Adulto |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT | Uso medio | Adulto |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT | Alto uso | Adulto Mayor |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT | Bajo uso | Adulto Mayor |
Celda 73 · código original
# Visualización de los segmentos por uso
def count_plot(df, columnas, nombre_df):
for col in columnas:
sns.countplot(data=df, x=col)
plt.xlabel(f"{col}")
plt.ylabel(f"Cantidad de repeticiones de {col}")
plt.title(f"Segmentación de Clientes '{col}' del df de {nombre_df}")
plt.show()
return df
count_plot(user_profile, user_profile[['grupo_uso', 'grupo_edad']], "los usuarios")
| user_id | cant_mensajes | cant_llamadas | cant_minutos_llamada | first_name | last_name | age | city | reg_date | plan | churn_date | grupo_uso | grupo_edad | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10000 | 7 | 3 | 23.70 | Carlos | Garcia | 38.0 | Medellín | 2022-01-01 00:00:00.000000000 | Basico | NaT | Uso medio | Adulto |
| 1 | 10001 | 5 | 10 | 33.18 | Mateo | Torres | 53.0 | NaN | 2022-01-01 06:34:17.914478619 | Basico | NaT | Alto uso | Adulto |
| 2 | 10002 | 5 | 2 | 10.74 | Sofia | Ramirez | 57.0 | CDMX | 2022-01-01 13:08:35.828957239 | Basico | NaT | Uso medio | Adulto |
| 3 | 10003 | 11 | 3 | 8.99 | Mateo | Ramirez | 69.0 | Bogotá | 2022-01-01 19:42:53.743435858 | Premium | NaT | Alto uso | Adulto Mayor |
| 4 | 10004 | 4 | 3 | 8.01 | Mateo | Torres | 63.0 | GDL | 2022-01-02 02:17:11.657914478 | Basico | NaT | Bajo uso | Adulto Mayor |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3994 | 13995 | 6 | 3 | 21.24 | Ana | Lopez | 60.0 | Medellín | 2024-12-29 21:42:48.342085520 | Basico | NaT | Uso medio | Adulto Mayor |
| 3995 | 13996 | 4 | 3 | 2.81 | Carlos | Ramirez | 24.0 | Medellín | 2024-12-30 04:17:06.256564144 | Premium | NaT | Bajo uso | Joven |
| 3996 | 13997 | 5 | 3 | 11.34 | Ana | Lopez | 58.0 | Bogotá | 2024-12-30 10:51:24.171042768 | Premium | NaT | Uso medio | Adulto |
| 3997 | 13998 | 6 | 6 | 22.95 | Mariana | Lopez | 57.0 | Bogotá | 2024-12-30 17:25:42.085521392 | Basico | NaT | Uso medio | Adulto |
| 3998 | 13999 | 3 | 6 | 26.46 | Ana | Lopez | 20.0 | Bogotá | 2024-12-31 00:00:00.000000000 | Basico | NaT | Uso medio | Joven |
3999 rows × 13 columns
Celda 74 · código original
# Visualización de los segmentos por edad
#YASin salida textual guardada; las gráficas se presentan en la galería.
Paso 7. Insight ejecutivo
Redacté un análisis ejecutivo sobre calidad de datos, predominio de adultos y concentración en uso medio. Propuse campañas orientadas a los segmentos adultos y explorar ofertas ajustadas a consumos intensivos.
El 6,95 % de alto uso ofrece un grupo concreto para investigar necesidades; el 19,45 % de bajo uso permite explorar propuestas más sencillas. Los planes ilimitados aparecen como una propuesta comercial del notebook, no como una conclusión financiera validada.
Paso 8. Documentación y GitHub
El cierre del proyecto contempla documentar el análisis en un notebook y un README y compartirlo en GitHub. El repositorio consultado contiene ambos archivos.
Esta página transforma las salidas guardadas en una presentación visual del Proyecto 6 (Sprint 7), sin sustituir el notebook ni modificar sus resultados. Los accesos al repositorio y a la descarga están al final.
Hallazgos que importan al negocio
La mayoría de los perfiles tiene uso medio, mientras que los adultos y adultos mayores concentran el 81,00 % de la base con actividad. Los extremos de consumo se preservaron para evitar perder señales relevantes.
Base: 3.999 perfiles. Los umbrales se aplican a conteos acumulados del periodo.
Base: 3.999 perfiles. Adulto = 30–59 años; adulto mayor = 60 años o más.
| Ciudad | Clientes | % de 3.435 conocidos |
|---|---|---|
| Bogotá | 808 | 23,52 % |
| CDMX | 730 | 21,25 % |
| Medellín | 616 | 17,93 % |
| GDL | 450 | 13,10 % |
| Cali | 424 | 12,34 % |
| MTY | 407 | 11,85 % |
Base geográfica: 3.435 clientes con ciudad conocida en users. Los 565 sin ciudad no se imputaron. Bogotá representa 23,52 % de las ubicaciones conocidas, no de toda la base.
Las colas derechas hacen que la media supere a la mediana. Por eso comuniqué ambas y mantuve los casos intensivos.
Oportunidades para validar
Propuestas inspiradas en el insight ejecutivo, separadas de los resultados estadísticos.
Investigar qué beneficios valoran los 2.943 perfiles de uso medio. Diseñar mensajes comerciales basados en necesidades observadas, sin asumir que todos desean el mismo plan.
Explorar necesidades de los 278 perfiles intensivos. Antes de ofrecer un upgrade, comprobar consumo por mes, plan actual, precios, costos y satisfacción.
Adaptar y probar campañas para los 3.239 perfiles de 30 años o más, sin excluir a jóvenes ni asumir preferencias solo por la edad.
Notas de rigor y límites del análisis
- Alcance temporal. El uso disponible corresponde al primer semestre de 2024, no al año completo. Los registros de clientes abarcan 2022–2024 y existen registros posteriores al periodo de uso; queda pendiente validar que cada evento ocurra después del registro del cliente.
- Universos distintos. Las participaciones por plan usan 4.000 clientes; las segmentaciones usan 3.999 perfiles con coincidencia en usage. El merge no incorpora como cero al usuario sin actividad registrada.
- Fechas ausentes. Los 50 eventos sin date se conservan en la agregación, porque el código agrupa por user_id sin filtrar por fecha. Impiden ubicar esos eventos con exactitud en el calendario.
- Campos por tipo de evento. Hay 17.924 duration no nulos frente a 17.908 llamadas, y 22.104 length no nulos frente a 22.092 SMS. Las salidas por tipo evidencian 16 SMS con duración y 12 llamadas con longitud. El código suma duration sin filtrar call; sus minutos agregados se presentan fielmente, pero conviene revisar esos casos antes de facturar.
- Missingness. Comparar nulos por tipo, edad o plan orienta el diagnóstico, pero no demuestra formalmente un mecanismo MCAR, MAR o MNAR. En duration y length predomina la no aplicabilidad estructural del campo.
- Edad y ciudad. Las medias originales de edad según ciudad faltante (32,51 frente a 42,96) se calcularon antes de corregir el sentinel -999. No las usé como prueba de que las personas mayores omiten más su ciudad.
- Bajas registradas. Los 466 usuarios con churn_date representan 11,65 % de users; los 3.534 sin fecha, 88,35 %. Son una descripción del campo, no una tasa de churn mensual ni una medición actual de retención.
- Valor económico. El notebook no calcula revenue, margen, consumo mensual ni pruebas de hipótesis. Los planes ilimitados y el upselling requieren validación adicional.
- Reproducibilidad. Esta web usa el código y las salidas guardadas del notebook adjunto, que muestra algunos estados de ejecución diferentes. No reejecuté el análisis: los tres CSV fuente no están incluidos entre los archivos consultados.
Insight ejecutivo original · celda 76
### Análisis ejecutivo ⚠️ **Problemas detectados en los datos** - los datos contaban con columnas mal configuradas e incompletas, con datos de tipo MCAR y MAR lo que provocaba confusión y la imposibilidad de buenos analisis. - Asi mismo se detectaron sentinels como ? en la columna "city" y -999 en la columna "age" - Los formatos de fecha estaban configurados como object 🔍 **Segmentos por Edad** - Se segmentó por edad y uso de los clientes mediante funciones y condiconales - Los resultados arrojaron que el grupo de edad que predomina son los adultos, seguido de los adultos mayor y un grupo dejoven reducido - como dato curioso descubierto con la IA los datos mas faltante de city (en df users): False 32.514868 True 42.961620 Muestra que los datos faltantes tienden a depender de la edad de los mayores, es decir a mayor edad menos probabilidad de registrar la ciudad 📊 **Segmentos por Nivel de Uso** - Los resultados arrojaron que el uso es predominantemente normal (tendencia al uso medio) - El uso alto es más bien raro, incluso más que el uso bajo ➡️ Esto sugiere que las personas tienden hacia un consumo relativamente normal con clientes ballenas a los que se les podría ofrecer planes adaptados a altos niveles de consumo, 💡 **Recomendaciones** - Focalizar las campañas de marketing hacia personas mayores de 30 años (adultos) para entender mejor el mainstream y las adaptaciones de marketing. - Los planes de telefonía ilimitada podrías servir como buena publicidad para los clientes, puesto que estos igual no consumen tanto
Las 10 gráficas del proyecto
Visualizaciones originales extraídas de las salidas del notebook: cuatro histogramas, cuatro boxplots y dos gráficos de segmentación. Se mantienen intactas, incluidos sus colores, ejes y títulos.
Pulsa una gráfica para ampliarla. Las imágenes conservan fondo blanco también en modo oscuro para no alterar la evidencia original. En la vista ampliada, usa las flechas del teclado para recorrerlas.
Histograma de edad
Edad de los perfiles, diferenciada por plan y con curva KDE. La distribución ocupa un rango amplio; el resumen de users registra edades de 18 a 79 años después de la limpieza.
No confundir una forma aproximadamente simétrica con una prueba de normalidad.
Histograma de mensajes
La mayor parte de los perfiles se concentra en consumos moderados. La cola derecha contiene clientes con más SMS; el máximo observado es 17.
Los mensajes se acumulan en el periodo disponible, no en un mes.
Histograma de llamadas
La distribución presenta una cola hacia consumos elevados. El perfil promedio tiene 4,48 llamadas y el máximo es 15.
La cantidad de llamadas y su duración describen dimensiones diferentes.
Histograma de minutos
La media de 23,32 minutos supera la mediana de 19,78. Un grupo de perfiles con duraciones altas alarga la cola derecha.
El código suma duration por usuario; ver la nota de consistencia sobre eventos SMS con duración.
Boxplot de edad
El boxplot del perfil no muestra valores fuera de sus bigotes. La imputación de edades no positivas con la mediana se realizó antes de esta visualización.
La ausencia de outliers no garantiza que todos los registros estén libres de errores.
Boxplot de mensajes
Q1 = 4 y Q3 = 7. El criterio IQR identifica consumos superiores a 11,5 mensajes como atípicos; el máximo es 17.
Se conservaron los extremos: representan comportamientos posibles.
Boxplot de llamadas
Q1 = 3 y Q3 = 6. Los perfiles con más de 10,5 llamadas se sitúan por encima del umbral de detección; el máximo es 15.
Ser atípico estadísticamente no convierte el dato en inválido.
Boxplot de minutos
Q1 = 11,12 y Q3 = 31,415. El máximo de 155,69 minutos equivale a aproximadamente 7,87 veces la mediana de 19,78.
La cifra máxima corresponde al total agregado del perfil, no a una única llamada.
Segmentación por uso
Uso medio: 2.943 (73,59 %); bajo uso: 778 (19,45 %); alto uso: 278 (6,95 %). La clasificación se basa en umbrales explícitos de mensajes y llamadas.
Alto uso no equivale automáticamente a alto valor económico.
Segmentación por edad
Adultos: 2.017 (50,44 %); adultos mayores: 1.222 (30,56 %); jóvenes: 760 (19,00 %). Los dos primeros grupos suman 81,00 % del perfil analizado.
La edad describe segmentos; no demuestra preferencias comerciales o propensión a abandonar.
Notebook y repositorio
Consulta el código completo, sus tablas y las visualizaciones, o descarga el notebook para trabajar en tu entorno de Python.
El repositorio contiene el notebook del Sprint 7 y el README del proyecto. La página presenta el análisis de telecomunicaciones mediante una estructura visual orientada a la consulta pública.
El botón principal permite descargar el notebook mediante un enlace directo de Google Drive. La disponibilidad depende de los permisos configurados en el archivo.
También incluí una copia del notebook adjunto dentro de este HTML, para disponer de una alternativa sin conexión.