Proyecto 6 · Sprint 7 · Python

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.

PythonPandasNumPySeabornMatplotlibJupyter

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.

Qué hice
  • 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.
Aporte como Data Analyst

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.

El resultado conecta calidad de datos, análisis exploratorio y comunicación de negocio. Las recomendaciones se presentan como hipótesis para validar, no como impactos económicos ya demostrados.
55,23 %
eventos son mensajes
22.092 SMS de 40.000 eventos
44,77 %
eventos son llamadas
17.908 llamadas de 40.000 eventos
64,88 %
clientes con plan Básico
2.595 de los 4.000 clientes
81,00 %
perfiles de 30 años o más
3.239 de los 3.999 perfiles
Datos de entrada y oferta de planes
CaracterísticaBásicoPremium
Clientes en users2.595 · 64,88 %1.405 · 35,13 %
Precio mensualUSD 12USD 25
SMS incluidos / mes100500
Minutos incluidos / mes100600
GB incluidos / mes520
Costo por GB extraUSD 1,20USD 1,00
Costo por SMS extraUSD 0,08USD 0,05
Costo por minuto extraUSD 0,10USD 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.

01

Paso 1. Carga y exploración

3 fuentes · 44.002 filas entre las tres tablas

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().

DatasetFilas × columnasContenido
plans2 × 8Tarifas, cupos y costos extra
users4.000 × 8Clientes, edad, ciudad, registro, plan y baja
usage40.000 × 6Eventos 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 plt

Sin 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
02

Paso 2. Diagnóstico de calidad

469 ciudades nulas · 96 “?” · 40 registros fuera del corte

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.

CampoHallazgo originalProporción
city469 nulos + 96 sentinels “?”11,73 % nulos iniciales
churn_date3.534 sin fecha de baja88,35 %
usage.date50 fechas ausentes0,125 %
duration22.076 nulos55,19 %
length17.896 nulos44,74 %
reg_date40 fechas en 2026, posteriores al corte 20241,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 sentinels
Cantidad 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 igual
0        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
03

Paso 3. Limpieza y fechas

Mediana de edad: 48 años · 565 ciudades sin identificar

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.

Los nulos de 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
04

Paso 4. Perfil estadístico por usuario

40.000 eventos → 3.999 perfiles con actividad

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 perfilMediaMedianaMáximo
Mensajes5,52517
Llamadas4,48415
Minutos agregados23,3219,78155,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
05

Paso 5. Distribuciones y outliers

8 gráficas · método IQR · extremos conservados

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.

VariableQ1Q3Límite superiorMáximo
Mensajes4711,517
Llamadas3610,515
Minutos11,1231,41561,8575155,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
06

Paso 6. Segmentación de clientes

73,59 % uso medio · 81,00 % tiene 30 años o más

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 usoUsuarios% de 3.999
Uso medio2.94373,59 %
Bajo uso77819,45 %
Alto uso2786,95 %
Segmento de edadUsuarios% de 3.999
Adulto2.01750,44 %
Adulto Mayor1.22230,56 %
Joven76019,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
#YA

Sin salida textual guardada; las gráficas se presentan en la galería.

07

Paso 7. Insight ejecutivo

Del consumo observado a hipótesis comerciales

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.

Para decidir precios, upgrades o planes ilimitados haría falta analizar consumo mensual, ingresos, costos, márgenes y respuesta de los clientes. El notebook no cuantifica un aumento de ingresos ni demuestra causalidad.
08

Paso 8. Documentación y GitHub

Notebook versionado · presentación de portafolio

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.

Segmentos por nivel de uso
Uso medio2.943 · 73,59 %
Bajo uso778 · 19,45 %
Alto uso278 · 6,95 %

Base: 3.999 perfiles. Los umbrales se aplican a conteos acumulados del periodo.

Segmentos por edad
Adulto2.017 · 50,44 %
Adulto Mayor1.222 · 30,56 %
Joven760 · 19,00 %

Base: 3.999 perfiles. Adulto = 30–59 años; adulto mayor = 60 años o más.

Ubicación de clientes conocidos
CiudadClientes% de 3.435 conocidos
Bogotá80823,52 %
CDMX73021,25 %
Medellín61617,93 %
GDL45013,10 %
Cali42412,34 %
MTY40711,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.

Qué muestran los consumos
Consumo típico de SMS5 mensajes · mediana
Consumo típico de llamadas4 llamadas · mediana
Duración agregada típica19,78 min · mediana
Mayor duración agregada155,69 min
Extremo / mediana7,87 veces

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.

Uso medio: propuesta principal

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.

Alto uso: oferta especializada

Explorar necesidades de los 278 perfiles intensivos. Antes de ofrecer un upgrade, comprobar consumo por mes, plan actual, precios, costos y satisfacción.

Adultos: comunicación segmentada

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
Conclusión: la segmentación convierte el consumo en grupos accionables. El siguiente paso es validar su rentabilidad y respuesta comercial con datos mensuales, no prometer mejoras que este análisis no midió.

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.

10 de 10 gráficas
Gráfica 01 / 10 · celda 52

Histograma de edad

48 años de mediana

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.

Gráfica 02 / 10 · celda 54

Histograma de mensajes

5 mensajes de mediana

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.

Gráfica 03 / 10 · celda 56

Histograma de llamadas

4 llamadas de mediana

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.

Gráfica 04 / 10 · celda 58

Histograma de minutos

155,69 minutos: máximo agregado

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.

Gráfica 05 / 10 · celda 61

Boxplot de edad

Sin outliers visibles 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.

Gráfica 06 / 10 · celda 61

Boxplot de mensajes

11,5: límite superior IQR

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.

Gráfica 07 / 10 · celda 61

Boxplot de llamadas

10,5: límite superior IQR

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.

Gráfica 08 / 10 · celda 61

Boxplot de minutos

61,8575: límite superior IQR

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.

Gráfica 09 / 10 · celda 73

Segmentación por uso

2.943 perfiles: uso medio

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.

Gráfica 10 / 10 · celda 73

Segmentación por edad

3.239 perfiles: 30 años o más

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.

Proyecto 6 · Telecom Analysis · ConnectaTel

Notebook: S7_Version_Estudiante_Project_ConnectaTel.ipynb
Portafolio de Andrés Bahamon · TripleTen Data Analytics

Abrir notebook
Código y documentación

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.

Descarga del notebook

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.

Fuentes: notebook del Sprint 7, salidas guardadas de sus 79 celdas y archivos del repositorio del proyecto. Diseño orientado a la presentación pública del portafolio. Porcentajes de segmentación calculados sobre 3.999 perfiles; cifras por plan y calidad, sobre 4.000 clientes. Esta presentación no modifica el notebook ni incorpora resultados de análisis nuevos. Fecha de preparación: 5 de octubre de 2026.

Gráfica del proyecto

Gráfica ampliada

↑