Proyecto Final · Sprint 12 · RappiPlus

De datos a decisiones de negocio

El proyecto final de RappiPlus integra limpieza auditable, rentabilidad, funnel, retención y experimentación. Python, SQL y Power BI conectan 24.600 pedidos completos con preguntas concretas sobre costos, concentración de ventas y experiencia de compra.

PythonPandasNumPySQLPostgreSQLStatsmodelsPower BISeabornMatplotlib
Ver visualizaciones

Notebook de consulta e informe PDF incorporados en el HTML. La copia del notebook excluye credenciales de conexión.

Resumen del proyecto final

El proceso recorre seis etapas analíticas y una entrega documentada. El periodo comercial mostrado corresponde a enero–junio de 2025, con tres países, siete productos y tres canales de referencia.

Qué integra el proyecto
  • Pedidos, catálogo y marketing con revisión y trazabilidad de calidad.
  • KPIs de revenue, costos, marketing y resultado económico.
  • Concentración de productos, países y canales.
  • Consultas SQL de eventos y ratios del funnel.
  • Retención por cohortes semanales de registro.
  • Prueba A/B de la nueva interfaz de checkout.
  • Dashboard ejecutivo y detalle de producto con drill-through.
Aporte como Data Analyst

El análisis distingue datos incompletos de datos recuperables, mantiene archivos de auditoría y evita imputar cifras que alteren artificialmente los resultados comerciales.

Combina métricas económicas con actividad de usuarios y evidencia experimental. Las recomendaciones separan anomalías observadas de explicaciones todavía no demostradas.

Facturación alta no garantiza margen alto, un ratio agregado no valida un funnel secuencial y una diferencia A/B positiva no implica significancia estadística.
24.600
pedidos completos analizados
100 duplicados y 400 auditables separados
80,04 %
ratio final de usuarios por evento
6.240 purchases / 7.796 first_visit
40,63 %
actividad retenida en semana 4
3.250 / 8.000 · tasa agregada ponderada
81,44 %
unidades de Laptop-Gaming-16GB
144.160 de 177.013 unidades
Dos definiciones de profit, un mismo revenue
IndicadorImporteDefinición / alcance
Revenue$51.836.375,14Suma de monto_total en 24.600 pedidos completos.
Costo de productos$43.078.678,80Costo unitario del catálogo × cantidad vendida.
Profit de Power BI$8.757.696,34Revenue − costo de productos; antes de marketing.
Marketing identificado$2.694.664,43Suma del gasto en marketing_clean.
Resultado tras costos y marketing$6.063.031,91Revenue − costo − marketing identificado.
Margen tras marketing11,70 %Resultado tras marketing / revenue.
Margen mostrado en BI16,89 %Profit antes de marketing / revenue.
Ticket promedio$2.107,17Revenue por pedido completo.

Power BI muestra $8.757.696,34 antes de marketing. Python calcula $6.063.031,91 al restar $2.694.664,43 de marketing identificado. Ambos resultados son compatibles, pero responden a definiciones distintas.

El proceso, de principio a fin

Limpieza, análisis comercial, consultas SQL, cohortes, prueba estadística y comunicación visual mantienen la secuencia del notebook.

01

Paso 1. Cargar, limpiar y auditar los datos

25.100 filas de pedidos → 24.600 completas + trazabilidad

La ingesta utiliza pedidos, catálogo y gasto de marketing. Una función de calidad revisa estructura, tipos, nulos, duplicados y categorías. Las fechas se convierten a datetime y se normalizan países y categorías.

Los valores no positivos en cantidad, precio y monto se convierten a nulos; el descuento cero se conserva porque es válido. La consistencia se revisa con cantidad × precio_unitario − monto_descuento y tolerancia de un centavo.

Se eliminan 100 repeticiones de pedidos. El catálogo permite completar 50 categorías; los datos imposibles de reconstruir se separan en archivos auditables, sin inventar países ni precios.

Fuente / tratamientoFilasDestino o criterio
Orders original25.10012 columnas; 100 repeticiones de pedidos.
Orders tras deduplicar25.000Un registro por id_pedido.
Orders completo24.600Base de ventas para los KPIs y Power BI.
Orders auditable400Registros incompletos conservados para trazabilidad.
Marketing original1.620Gasto con canal identificado o ausente.
Marketing completo1.519Registros con canal conocido.
Marketing auditable101Canal nulo, gasto conservado para revisión.
Catálogo7 productosReferencia de categoría, proveedor y costo unitario.

Los 400 pedidos auditables no se borran del rastro del proyecto. Marketing con canal ausente también se conserva aparte; su gasto no forma parte del indicador de marketing_clean.

Ver código y resultados documentados

Celda 14 · código documentado

# Validar y convertir fechas al formato correcto
orders['fecha_hora_pedido'] = pd.to_datetime(orders['fecha_hora_pedido'])
marketing['fecha'] = pd.to_datetime(marketing['fecha'])

Celda 22 · código documentado

# LIMPIEZA Y ESTANDARIZACIÓN DEL VARIABLES NUMERICAS EN ORDERS

# Reemplazar valores negativos por la mediana de cantidad y monto total
orders.info()
# ============================================================
# REEMPLAZAR VALORES MENORES O IGUALES A CERO POR NaN
# ============================================================

# Crear una lista con las columnas numéricas donde
# los valores deben ser mayores que cero.
#
# No se incluye monto_descuento porque un descuento de 0 es válido.
columnas_a_limpiar = [
    'cantidad',
    'precio_unitario',
    'monto_total'
]

# Recorrer cada columna de la lista.
for columna in columnas_a_limpiar:
    
    # Buscar los valores menores o iguales a cero.
    #
    # <= 0 significa:
    # menor que cero O igual a cero.
    #
    # np.nan representa un valor faltante.
    #
    # loc permite modificar únicamente las filas que cumplen
    # la condición indicada.
    orders.loc[orders[columna] <= 0, columna] = np.nan

#Verificar cambios
explorar(orders, col_num_orders)
valores_nulos(orders, col_num_orders)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 25100 entries, 0 to 25099
Data columns (total 12 columns):
 #   Column              Non-Null Count  Dtype         
---  ------              --------------  -----         
 0   id_pedido           25100 non-null  object        
 1   id_usuario          25100 non-null  object        
 2   fecha_hora_pedido   25100 non-null  datetime64[ns]
 3   pais                24800 non-null  object        
 4   dispositivo         25080 non-null  object        
 5   fuente_referencia   25070 non-null  object        
 6   nombre_producto     25070 non-null  object        
 7   categoria_producto  25020 non-null  object        
 8   cantidad            25050 non-null  float64       
 9   precio_unitario     25050 non-null  float64       
 10  monto_descuento     25050 non-null  float64       
 11  monto_total         25100 non-null  float64       
dtypes: datetime64[ns](1), float64(4), object(7)
memory usage: 2.3+ MB

Estadisticas descriptivas de cantidad
Mediana:  2.0
count    25046.000000
mean         7.094067
std        296.300643
min          1.000000
25%          1.000000
50%          2.000000
75%          2.000000
max      20000.000000
Name: cantidad, dtype: float64
___________________________

Estadisticas descriptivas de precio_unitario
Mediana:  258.71500000000003
count    25050.000000
mean       259.305549
std        138.726461
min         20.030000
25%        138.377500
50%        258.715000
75%        380.332500
max        499.960000
Name: precio_unitario, dtype: float64
___________________________

Estadisticas descriptivas de monto_descuento
Mediana:  0.0
count    25050.000000
mean         4.500798
std          5.223010
min          0.000000
25%          0.000000
50%          0.000000
75%         10.000000
max         15.000000
Name: monto_descuento, dtype: float64
___________________________

Estadisticas descriptivas de monto_total
Mediana:  341.80499999999995
count    2.509600e+04
mean     2.073056e+03
std      9.895783e+04
min      5.240000e+00
25%      1.805925e+02
50%      3.418050e+02
75%      5.185950e+02
max      8.840200e+06
Name: monto_total, dtype: float64
___________________________
Cantidad de valores nulos en columna cantidad
54
Proporción de valores nulos en columna cantidad
0.002151394422310757
_____________________________________
Cantidad de valores nulos en columna precio_unitario
50
Proporción de valores nulos en columna precio_unitario
0.00199203187250996
_____________________________________
Cantidad de valores nulos en columna monto_descuento
50
Proporción de valores nulos en columna monto_descuento
0.00199203187250996
_____________________________________
Cantidad de valores nulos en columna monto_total
4
Proporción de valores nulos en columna monto_total
0.00015936254980079682
_____________________________________

Celda 26 · código documentado

# ============================================================
# VERIFICAR CONSISTENCIA ENTRE CANTIDAD, PRECIO Y MONTO TOTAL
# ============================================================

# ------------------------------------------------------------
# 1. CALCULAR EL MONTO TOTAL ESPERADO
# ------------------------------------------------------------
# La fórmula es:
#
# cantidad * precio_unitario - monto_descuento
#
# El resultado se guarda en una nueva columna llamada
# 'monto_total_calculado'.
orders['monto_total_calculado'] = (
    orders['cantidad'] * orders['precio_unitario']
) - orders['monto_descuento']


# ------------------------------------------------------------
# 2. CALCULAR LA DIFERENCIA ENTRE AMBOS MONTOS
# ------------------------------------------------------------
# Restamos el monto informado en la base de datos menos
# el monto calculado con la fórmula.
#
# Si la diferencia es 0, el monto es consistente.
# Si la diferencia es distinta de 0, puede haber inconsistencia.
orders['diferencia_monto'] = (
    orders['monto_total'] - orders['monto_total_calculado']
)


# ------------------------------------------------------------
# 3. IDENTIFICAR LAS FILAS INCONSISTENTES
# ------------------------------------------------------------
# Identificar las filas que tienen todos los valores necesarios
# para verificar la fórmula:
# cantidad * precio_unitario - monto_descuento.
filas_completas = orders[
    [
        'cantidad',
        'precio_unitario',
        'monto_descuento',
        'monto_total'
    ]
].notna().all(axis=1)

# Redondear la diferencia a dos decimales.
# Con esto, 0.0100000000005 se convierte en 0.01.
diferencia_redondeada = orders['diferencia_monto'].round(2)

# Marcar como inconsistentes únicamente diferencias de 2 centavos
# o más, tanto positivas como negativas.
#
# 0.01 y -0.01 se consideran diferencias aceptables por redondeo.
filas_inconsistentes = (
    filas_completas
    & (
        (diferencia_redondeada >= 0.02)
        | (diferencia_redondeada <= -0.02)
    )
)

# ------------------------------------------------------------
# 4. CREAR UNA TABLA SOLO CON LAS INCONSISTENCIAS
# ------------------------------------------------------------
# Esta tabla muestra únicamente los pedidos donde la fórmula
# no coincide con el monto_total registrado.
inconsistencias_montos = orders[filas_inconsistentes]

# Seleccionar las columnas necesarias para revisar cada caso.
inconsistencias_montos = inconsistencias_montos[
    [
        'id_pedido',
        'cantidad',
        'precio_unitario',
        'monto_descuento',
        'monto_total',
        'monto_total_calculado',
        'diferencia_monto'
    ]
]


# ------------------------------------------------------------
# 5. MOSTRAR EL REPORTE
# ------------------------------------------------------------
print('----- REPORTE DE CONSISTENCIA DE MONTOS -----')

# Mostrar la cantidad total de filas revisadas.
print('\nCantidad total de pedidos revisados:')
print(orders.shape[0])

# Mostrar la cantidad de filas inconsistentes.
print('\nCantidad de pedidos con montos inconsistentes:')
print(inconsistencias_montos.shape[0])

# Mostrar las primeras 10 inconsistencias para investigarlas.
print('\nPrimeras 10 filas con montos inconsistentes:')
print(inconsistencias_montos.head(10))
----- REPORTE DE CONSISTENCIA DE MONTOS -----

Cantidad total de pedidos revisados:
25100

Cantidad de pedidos con montos inconsistentes:
0

Primeras 10 filas con montos inconsistentes:
Empty DataFrame
Columns: [id_pedido, cantidad, precio_unitario, monto_descuento, monto_total, monto_total_calculado, diferencia_monto]
Index: []

Celda 31 · código documentado

# ============================================================
# ELIMINAR DUPLICADOS DEL DATASET ORDERS
# ============================================================

# ------------------------------------------------------------
# 1. REVISAR DUPLICADOS COMPLETOS
# ------------------------------------------------------------
# duplicated() revisa si una fila completa aparece repetida.
#
# keep=False marca como True todas las apariciones de una fila
# repetida, incluida la primera.
filas_duplicadas_completas = orders.duplicated(
    keep=False
)

# Mostrar cuántas filas participan en duplicados completos.
print('Cantidad de filas duplicadas completamente:')
print(filas_duplicadas_completas.sum())

# Mostrar algunos ejemplos.
print('\nEjemplos de duplicados completos:')
print(orders[filas_duplicadas_completas].head())


# ------------------------------------------------------------
# 2. REVISAR DUPLICADOS POR ID_PEDIDO
# ------------------------------------------------------------
# id_pedido debe identificar un pedido individual.
#
# keep=False marca todos los registros cuyo id_pedido aparece
# más de una vez.
ids_pedido_duplicados = orders['id_pedido'].duplicated(
    keep=False
)

# Mostrar cuántas filas pertenecen a IDs repetidos.
print('\nCantidad de filas con id_pedido repetido:')
print(ids_pedido_duplicados.sum())

# Mostrar algunos ejemplos de IDs repetidos.
print('\nEjemplos de id_pedido repetidos:')
print(
    orders[ids_pedido_duplicados]
    .sort_values('id_pedido')
    .head(10)
)


# ------------------------------------------------------------
# 3. ELIMINAR DUPLICADOS COMPLETOS
# ------------------------------------------------------------
# drop_duplicates() elimina las filas que son idénticas
# en todas sus columnas.
#
# keep='first' conserva la primera aparición y elimina
# las repeticiones posteriores.
orders = orders.drop_duplicates(
    keep='first'
)


# ------------------------------------------------------------
# 4. ELIMINAR REPETICIONES DEL ID_PEDIDO
# ------------------------------------------------------------
# subset=['id_pedido'] indica que la revisión se realiza
# únicamente usando la columna id_pedido.
#
# keep='first' conserva el primer registro de cada pedido
# y elimina cualquier repetición posterior.
orders = orders.drop_duplicates(
    subset=['id_pedido'],
    keep='first'
)


# ------------------------------------------------------------
# 5. REINICIAR EL ÍNDICE
# ------------------------------------------------------------
# Después de eliminar filas, el índice puede conservar saltos.
#
# reset_index(drop=True) crea un índice nuevo:
# 0, 1, 2, 3, ...
#
# drop=True evita que el índice anterior se convierta
# en una columna adicional.
orders = orders.reset_index(
    drop=True
)


# ------------------------------------------------------------
# 6. COMPROBAR QUE YA NO HAY DUPLICADOS
# ------------------------------------------------------------
print('\n----- RESULTADO FINAL -----')
resumen_calidad(orders)
Cantidad de filas duplicadas completamente:
200

Ejemplos de duplicados completos:
       id_pedido id_usuario fecha_hora_pedido       pais dispositivo  \
734    order_734  user_6347        2025-05-02   Colombia     desktop   
812    order_812  user_1530        2025-03-31  Argentina     desktop   
974    order_974  user_3262        2025-05-04  Argentina     desktop   
1361  order_1361   user_507        2025-06-27     Mexico     desktop   
1735  order_1735   user_728        2025-05-13     Mexico      mobile   

     fuente_referencia       nombre_producto categoria_producto  cantidad  \
734            organic        Blender-XL-Red              Hogar       1.0   
812            organic  Tablet-Standard-64GB        Electronica       1.0   
974        paid_search        Blender-XL-Red              Hogar       2.0   
1361       paid_search    Laptop-Gaming-16GB        Electronica       2.0   
1735            social  Tablet-Standard-64GB        Electronica       2.0   

      precio_unitario  monto_descuento  monto_total  monto_total_calculado  \
734             58.00              0.0        58.00                  58.00   
812            167.32             10.0       157.32                 157.32   
974            268.58              5.0       532.16                 532.16   
1361           239.61              0.0       479.22                 479.22   
1735           419.04              0.0       838.08                 838.08   

      diferencia_monto  
734                0.0  
812                0.0  
974                0.0  
1361               0.0  
1735               0.0  

Cantidad de filas con id_pedido repetido:
200

Ejemplos de id_pedido repetidos:
         id_pedido id_usuario fecha_hora_pedido       pais dispositivo  \
25023  order_10082   user_690        2025-02-17  Argentina     desktop   
10082  order_10082   user_690        2025-02-17  Argentina     desktop   
25037  order_10709  user_6783        2025-02-16   Colombia      mobile   
10709  order_10709  user_6783        2025-02-16   Colombia      mobile   
25065  order_10829  user_7697        2025-01-23  Argentina      mobile   
10829  order_10829  user_7697        2025-01-23  Argentina      mobile   
10856  order_10856  user_1899        2025-03-18   Colombia     desktop   
25044  order_10856  user_1899        2025-03-18   Colombia     desktop   
11449  order_11449  user_1480        2025-03-21     Mexico     desktop   
25083  order_11449  user_1480        2025-03-21     Mexico     desktop   

      fuente_referencia     nombre_producto categoria_producto  cantidad  \
25023            social   Sneakers-Urban-42               Moda       2.0   
10082            social   Sneakers-Urban-42               Moda       2.0   
25037           organic     Jacket-Winter-M               Moda       1.0   
10709           organic     Jacket-Winter-M               Moda       1.0   
25065            social     Phone-Pro-128GB        Electronica       2.0   
10829            social     Phone-Pro-128GB        Electronica       2.0   
10856            social  Laptop-Gaming-16GB        Electronica       1.0   
25044            social  Laptop-Gaming-16GB        Electronica       1.0   
11449       paid_search    Vacuum-Pro-Black              Hogar       1.0   
25083       paid_search    Vacuum-Pro-Black              Hogar       1.0   

       precio_unitario  monto_descuento  monto_total  monto_total_calculado  \
25023           221.34              0.0       442.67                 442.68   
10082           221.34              0.0       442.67                 442.68   
25037           170.10             10.0       160.10                 160.10   
10709           170.10             10.0       160.10                 160.10   
25065           115.54              0.0       231.08                 231.08   
10829           115.54              0.0       231.08                 231.08   
10856            72.53              0.0        72.53                  72.53   
25044            72.53              0.0        72.53                  72.53   
11449            60.55              5.0        55.55                  55.55   
25083            60.55              5.0        55.55                  55.55   

       diferencia_monto  
25023             -0.01  
10082             -0.01  
25037              0.00  
10709              0.00  
25065              0.00  
10829              0.00  
10856              0.00  
25044              0.00  
11449              0.00  
25083              0.00  

----- RESULTADO FINAL -----
----- INFORMACIÓN GENERAL -----

Número de filas: 25000
Número de columnas: 14

----- NOMBRES DE COLUMNAS -----
Index(['id_pedido', 'id_usuario', 'fecha_hora_pedido', 'pais', 'dispositivo',
       'fuente_referencia', 'nombre_producto', 'categoria_producto',
       'cantidad', 'precio_unitario', 'monto_descuento', 'monto_total',
       'monto_total_calculado', 'diferencia_monto'],
      dtype='object')

----- TIPOS DE DATOS -----
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 25000 entries, 0 to 24999
Data columns (total 14 columns):
 #   Column                 Non-Null Count  Dtype         
---  ------                 --------------  -----         
 0   id_pedido              25000 non-null  object        
 1   id_usuario             25000 non-null  object        
 2   fecha_hora_pedido      25000 non-null  datetime64[ns]
 3   pais                   24700 non-null  object        
 4   dispositivo            24980 non-null  object        
 5   fuente_referencia      24970 non-null  object        
 6   nombre_producto        24970 non-null  object        
 7   categoria_producto     24920 non-null  object        
 8   cantidad               24946 non-null  float64       
 9   precio_unitario        24950 non-null  float64       
 10  monto_descuento        24950 non-null  float64       
 11  monto_total            24996 non-null  float64       
 12  monto_total_calculado  24946 non-null  float64       
 13  diferencia_monto       24946 non-null  float64       
dtypes: datetime64[ns](1), float64(6), object(7)
memory usage: 2.7+ MB
None

----- VALORES FALTANTES -----
pais                     300
dispositivo               20
fuente_referencia         30
nombre_producto           30
categoria_producto        80
cantidad                  54
precio_unitario           50
monto_descuento           50
monto_total                4
monto_total_calculado     54
diferencia_monto          54
dtype: int64

----- FILAS DUPLICADAS COMPLETAMENTE -----
Cantidad de filas duplicadas: 0

----- DUPLICADOS EN ID_PEDIDO -----
Cantidad de id_pedido repetidos: 0
Cantidad de id_pedido únicos: 25000

----- PRIMERAS CINCO FILAS -----
  id_pedido id_usuario fecha_hora_pedido       pais dispositivo  \
0   order_0  user_6993        2025-05-22  Argentina     desktop   
1   order_1  user_1329        2025-06-15     Mexico     desktop   
2   order_2  user_3194        2025-05-02  Argentina     desktop   
3   order_3  user_4510        2025-06-09   Colombia      mobile   
4   order_4  user_5044        2025-03-30  Argentina     desktop   

  fuente_referencia       nombre_producto categoria_producto  cantidad  \
0           organic       Jacket-Winter-M               Moda       2.0   
1       paid_search  Tablet-Standard-64GB        Electronica       1.0   
2            social        Blender-XL-Red              Hogar       2.0   
3            social  Tablet-Standard-64GB        Electronica       1.0   
4       paid_search        Blender-XL-Red              Hogar       1.0   

   precio_unitario  monto_descuento  monto_total  monto_total_calculado  \
0           332.69              0.0       665.37                 665.38   
1           176.86              5.0       171.86                 171.86   
2           102.99             10.0       195.99                 195.98   
3           257.87             15.0       242.87                 242.87   
4           336.28              0.0       336.28                 336.28   

   diferencia_monto  
0             -0.01  
1              0.00  
2              0.01  
3              0.00  
4              0.00  

----- VALORES UNICOS EN CADA COLUMNA -----
id_pedido                25000
id_usuario                7642
fecha_hora_pedido          181
pais                         6
dispositivo                  2
fuente_referencia            3
nombre_producto              7
categoria_producto           3
cantidad                     4
precio_unitario          19543
monto_descuento              4
monto_total              21532
monto_total_calculado    20942
diferencia_monto            32
dtype: int64

----- DETALLE DE COLUMNAS CATEGÓRICAS -----

Columna: id_pedido
order_8667     1
order_18261    1
order_4740     1
order_15476    1
order_6603     1
              ..
order_19233    1
order_14754    1
order_13009    1
order_22781    1
order_5482     1
Name: id_pedido, Length: 25000, dtype: int64

Columna: id_usuario
user_7769    11
user_5748    10
user_3267    10
user_6272    10
user_7975    10
             ..
user_6627     1
user_2516     1
user_2186     1
user_7818     1
user_7761     1
Name: id_usuario, Length: 7642, dtype: int64

Columna: pais
Colombia     7481
Mexico       7478
Argentina    7259
mexico        863
colombia      823
argentina     796
NaN           300
Name: pais, dtype: int64

Columna: dispositivo
desktop    12711
mobile     12269
NaN           20
Name: dispositivo, dtype: int64

Columna: fuente_referencia
social         8402
organic        8288
paid_search    8280
NaN              30
Name: fuente_referencia, dtype: int64

Columna: nombre_producto
Blender-XL-Red          4179
Jacket-Winter-M         4176
Vacuum-Pro-Black        4176
Sneakers-Urban-42       4148
Laptop-Gaming-16GB      2782
Tablet-Standard-64GB    2769
Phone-Pro-128GB         2740
NaN                       30
Name: nombre_producto, dtype: int64

Columna: categoria_producto
Hogar          8346
Moda           8295
Electronica    8279
NaN              80
Name: categoria_producto, dtype: int64

Celda 36 · código documentado

# Se imputan las categorias de producto a continuación dado que se cuenta con la info completa

# Crear una tabla de referencia: para cada producto, obtener su categoría
# set_index convierte 'nombre_producto' en el índice (la clave de búsqueda)
# ['categoria_producto'] extrae solo la columna de categorías
mapa_categoria = catalog.set_index('nombre_producto')['categoria_producto']

# Rellenar únicamente las categorías que están vacías (NaN)
# pedidos['nombre_producto'].map(mapa_categoria) busca la categoría de cada producto
# fillna reemplaza solo los valores faltantes, dejando intactos los que ya existen
orders['categoria_producto'] = orders['categoria_producto'].fillna(
    orders['nombre_producto'].map(mapa_categoria))

orders['categoria_producto'] = orders['categoria_producto'].replace({
    'Electrónica':'Electronica'})

# Se estandarizó la categoria Electronica

orders.info()
orders[orders['categoria_producto'].isna()]
orders['categoria_producto'].value_counts(dropna=False)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 25000 entries, 0 to 24999
Data columns (total 14 columns):
 #   Column                 Non-Null Count  Dtype         
---  ------                 --------------  -----         
 0   id_pedido              25000 non-null  object        
 1   id_usuario             25000 non-null  object        
 2   fecha_hora_pedido      25000 non-null  datetime64[ns]
 3   pais                   24700 non-null  object        
 4   dispositivo            24980 non-null  object        
 5   fuente_referencia      24970 non-null  object        
 6   nombre_producto        24970 non-null  object        
 7   categoria_producto     24970 non-null  object        
 8   cantidad               24946 non-null  float64       
 9   precio_unitario        24950 non-null  float64       
 10  monto_descuento        24950 non-null  float64       
 11  monto_total            24996 non-null  float64       
 12  monto_total_calculado  24946 non-null  float64       
 13  diferencia_monto       24946 non-null  float64       
dtypes: datetime64[ns](1), float64(6), object(7)
memory usage: 2.7+ MB
Hogar          8355
Moda           8324
Electronica    8291
NaN              30
Name: categoria_producto, dtype: int64

Celda 38 · código documentado

# Unificar la escritura de la categoría de tecnología
# En algunos registros aparece 'mexico' y en otros 'Mexico'
# replace cambia todos los nombres a un solo estandar dado
orders['pais'] = orders['pais'].replace({
    'mexico':'Mexico', 'colombia':'Colombia', 'argentina':'Argentina'})

orders['pais'].value_counts(dropna=False)
Mexico       8341
Colombia     8304
Argentina    8055
NaN           300
Name: pais, dtype: int64

Celda 41 · código documentado

# ============================================================
# FUNCIÓN PARA SEPARAR FILAS CON VALORES NULOS
# ============================================================

def separar_valores_nulos(df, columnas_importantes):
    
    # --------------------------------------------------------
    # 1. IDENTIFICAR FILAS QUE TIENEN ALGÚN VALOR NULO
    # --------------------------------------------------------
    # df[columnas_importantes] selecciona solo las columnas
    # que deben contener datos.
    #
    # isna() marca con True los valores NaN.
    #
    # any(axis=1) revisa cada fila:
    # devuelve True si al menos una de las columnas tiene NaN.
    filas_con_nulos = df[columnas_importantes].isna().any(axis=1)
    
    
    # --------------------------------------------------------
    # 2. CREAR EL DATASET AUDITABLE
    # --------------------------------------------------------
    # Guardar todas las filas que tienen por lo menos un NaN.
    #
    # copy() crea una copia independiente del DataFrame.
    datos_auditables_nulos = df[filas_con_nulos].copy()
    
    
    # --------------------------------------------------------
    # 3. CONSERVAR SOLO FILAS COMPLETAS
    # --------------------------------------------------------
    # El símbolo ~ significa "NO".
    #
    # Por eso, ~filas_con_nulos conserva las filas que no tienen
    # valores nulos en las columnas importantes.
    datos_sin_nulos = df[~filas_con_nulos].copy()
    
    
    # --------------------------------------------------------
    # 4. REINICIAR EL ÍNDICE DEL DATASET PRINCIPAL
    # --------------------------------------------------------
    # Crea una nueva numeración consecutiva: 0, 1, 2, 3, ...
    #
    # drop=True evita guardar el índice antiguo como columna.
    datos_sin_nulos = datos_sin_nulos.reset_index(drop=True)
    
    
    # --------------------------------------------------------
    # 5. MOSTRAR UN RESUMEN
    # --------------------------------------------------------
    print('----- RESUMEN DE SEPARACIÓN DE NULOS -----')
    
    print('\nFilas originales:')
    print(df.shape[0])
    
    print('\nFilas enviadas al dataset auditable:')
    print(datos_auditables_nulos.shape[0])
    
    print('\nFilas que permanecen en el dataset principal:')
    print(datos_sin_nulos.shape[0])
    
    print('\nValores nulos encontrados en el dataset auditable:')
    print(datos_auditables_nulos[columnas_importantes].isna().sum())
    
    
    # --------------------------------------------------------
    # 6. DEVOLVER LOS DOS DATASETS
    # --------------------------------------------------------
    # return entrega los dos DataFrames que creó la función.
    return datos_sin_nulos, datos_auditables_nulos

Celda 42 · código documentado

# La función devuelve dos resultados:
#
# 1. orders: filas completas que se conservarán para el análisis.
# 2. orders_auditable_nulos: filas con NaN para trazabilidad.
orders, orders_auditable_nulos = separar_valores_nulos(
    orders,
    columnas_importantes_orders
)


## Definir la columna obligatoria para el análisis de marketing.
#
# canal es necesario para calcular gasto por canal.
columnas_importantes_marketing = [
    'canal'
]

# La función devuelve dos resultados:
#
# 1. marketing: registros que tienen canal identificado.
# 2. marketing_auditable_nulos: registros cuyo canal es NaN.
marketing, marketing_auditable_nulos = separar_valores_nulos(
    marketing,
    columnas_importantes_marketing
)
----- RESUMEN DE SEPARACIÓN DE NULOS -----

Filas originales:
25000

Filas enviadas al dataset auditable:
400

Filas que permanecen en el dataset principal:
24600

Valores nulos encontrados en el dataset auditable:
pais                  300
dispositivo            20
fuente_referencia      30
nombre_producto        30
categoria_producto     30
cantidad               54
precio_unitario        50
monto_descuento        50
monto_total             4
dtype: int64
----- RESUMEN DE SEPARACIÓN DE NULOS -----

Filas originales:
1620

Filas enviadas al dataset auditable:
101

Filas que permanecen en el dataset principal:
1519

Valores nulos encontrados en el dataset auditable:
canal    101
dtype: int64

Celda 49 · código documentado

# exportar datasets
orders.to_csv('orders_clean.csv', index=False)
orders_auditable_nulos.to_csv('orders_auditable_nulos.csv', index=False)
catalog.to_csv('catalog_clean.csv', index=False)
marketing.to_csv('marketing_clean.csv', index=False)
marketing_auditable_nulos.to_csv('marketing_auditable_nulos.csv', index=False)
02

Paso 2. Evaluar rentabilidad y comportamiento de ventas

Revenue $51,84 M · resultado tras marketing $6,06 M

Se unen pedidos con catálogo por nombre_producto, se calcula el costo de las unidades vendidas y se incorpora el marketing identificado. El resultado es positivo dentro del alcance de estos datos.

IndicadorImporteDefinición / alcance
Revenue$51.836.375,14Suma de monto_total en 24.600 pedidos completos.
Costo de productos$43.078.678,80Costo unitario del catálogo × cantidad vendida.
Profit de Power BI$8.757.696,34Revenue − costo de productos; antes de marketing.
Marketing identificado$2.694.664,43Suma del gasto en marketing_clean.
Resultado tras costos y marketing$6.063.031,91Revenue − costo − marketing identificado.
Margen tras marketing11,70 %Resultado tras marketing / revenue.
Margen mostrado en BI16,89 %Profit antes de marketing / revenue.
Ticket promedio$2.107,17Revenue por pedido completo.

El ticket promedio es $2.107,17 y la cantidad media 7,20 unidades por pedido. Laptop-Gaming-16GB acumula 144.160 unidades, una concentración que exige revisar las órdenes de gran volumen.

CanalGasto identificadoParticipación
social$918.043,2134,1 %
organic$913.533,0133,9 %
paid_search$863.088,2132,0 %
El profit de la tarjeta Power BI es anterior a marketing; el resultado de Python resta marketing. No se presentan como el mismo indicador ni como utilidad neta después de todos los gastos empresariales.
Ver código y resultados documentados

Celda 53 · código documentado

# Cargar los datos
catalog_clean = pd.read_csv('catalog_clean.csv')
marketing_clean = pd.read_csv('marketing_clean.csv')
orders_clean = pd.read_csv('orders_clean.csv')


# ANÁLISIS DE RENTABILIDAD

# ¿Cuál es el ingreso total (revenue)?
ingreso_total = orders_clean['monto_total'].sum()

# =============================================================================
# ¿Cuál es el costo total?
# Unir catálogo con órdenes para obtener el costo de cada producto vendido
orders_clean_con_costo = orders_clean.merge(
    catalog_clean[['nombre_producto', 'costo_unitario']],
    on='nombre_producto',
    how='left'
)

# Calcular el costo de cada orden y sumar todos
costo_total = (orders_clean_con_costo['costo_unitario'] * orders_clean_con_costo['cantidad']).sum()

# =============================================================================
# ¿Cuánto se ha invertido en marketing?

gasto_marketing_total = marketing_clean['gasto'].sum()

# =============================================================================
# ¿El negocio es rentable? (calcular profit)
# =============================================================================
profit = ingreso_total - costo_total - gasto_marketing_total
margen_profit = (profit / ingreso_total) * 100

# =============================================================================
# Resultados
# =============================================================================
print("="*60)
print("RESULTADOS")
print("="*60)
print(f"Ingreso total (revenue):    ${ingreso_total:,.2f}")
print(f"Costo total:                ${costo_total:,.2f}")
print(f"Gasto en marketing:         ${gasto_marketing_total:,.2f}")
print(f"Profit:                     ${profit:,.2f}")
print(f"Margen de profit:           {margen_profit:.2f}%")
print("="*60)

if profit > 0:
    print("El negocio es rentable")
else:
    print("El negocio no es rentable")
============================================================
RESULTADOS
============================================================
Ingreso total (revenue):    $51,836,375.14
Costo total:                $43,078,678.80
Gasto en marketing:         $2,694,664.43
Profit:                     $6,063,031.91
Margen de profit:           11.70%
============================================================
El negocio es rentable

Celda 54 · código documentado

# =============================================================================
# PARTE 2: COMPORTAMIENTO DE VENTAS
# =============================================================================

# =============================================================================
# ¿Cuál es el ticket promedio por orden?
# Ticket promedio = monto total de todas las órdenes / número de órdenes
ticket_promedio = orders_clean['monto_total'].mean()

# =============================================================================
# ¿Cuál es la cantidad promedio de productos por orden?
# Sumamos todas las cantidades y dividimos entre el número de órdenes
cantidad_promedio = orders_clean['cantidad'].mean()

# =============================================================================
# ¿Cuál es el producto más vendido?
# Agrupamos por producto y sumamos las cantidades
ventas_por_producto = orders_clean.groupby('nombre_producto')['cantidad'].sum()
producto_mas_vendido = ventas_por_producto.idxmax()
cantidad_producto_mas_vendido = ventas_por_producto.max()

# =============================================================================
# ¿Cuánto se ha gastado en marketing por canal?
# Agrupamos por canal y sumamos los gastos
gasto_por_canal = marketing_clean.groupby('canal')['gasto'].sum().sort_values(ascending=False)

# =============================================================================
# Resultados
print("="*60)
print("PARTE 2: COMPORTAMIENTO DE VENTAS")
print("="*60)

print("\nTICKET PROMEDIO POR ORDEN")
print(f"   Ticket promedio: ${ticket_promedio:,.2f}")

print("\nCANTIDAD PROMEDIO DE PRODUCTOS POR ORDEN")
print(f"   Cantidad promedio: {cantidad_promedio:.2f} productos")


print("\nPRODUCTO MÁS VENDIDO")
print("top 5 productos más vendidos")
top_5_productos = orders_clean.groupby('nombre_producto')['cantidad'].sum().sort_values(ascending=False).head(5)
print(top_5_productos)
print(" ____________________________ ")
print(f"Producto mas vendido: {producto_mas_vendido}")
print(f"Cantidad vendida: {cantidad_producto_mas_vendido:,.0f} unidades")


print("\nGASTO EN MARKETING POR CANAL")
for canal, gasto in gasto_por_canal.items():
    porcentaje = (gasto / gasto_por_canal.sum()) * 100         # gasto = el gasto de UN canal y gasto_por_canal.sum() = la suma de TODOS los canales 
    print(f"   {canal}: ${gasto:,.2f} ({porcentaje:.1f}%)")


print("\n" + "="*60)
============================================================
PARTE 2: COMPORTAMIENTO DE VENTAS
============================================================

TICKET PROMEDIO POR ORDEN
   Ticket promedio: $2,107.17

CANTIDAD PROMEDIO DE PRODUCTOS POR ORDEN
   Cantidad promedio: 7.20 productos

PRODUCTO MÁS VENDIDO
top 5 productos más vendidos
nombre_producto
Laptop-Gaming-16GB    144160.0
Vacuum-Pro-Black        6203.0
Jacket-Winter-M         6185.0
Blender-XL-Red          6184.0
Sneakers-Urban-42       6085.0
Name: cantidad, dtype: float64
 ____________________________ 
Producto mas vendido: Laptop-Gaming-16GB
Cantidad vendida: 144,160 unidades

GASTO EN MARKETING POR CANAL
   social: $918,043.21 (34.1%)
   organic: $913,533.01 (33.9%)
   paid_search: $863,088.21 (32.0%)

============================================================
03

Paso 3. Analizar el funnel de conversión con SQL

80,04 % final · mayor caída aparente en información de pago

Se consulta events y se cuenta COUNT(DISTINCT id_usuario) por evento. Con CTE, subconsultas y NULLIF se calculan razones entre etapas y porcentajes complementarios.

Transición del funnelUsuarios de etapa inicial → siguienteConversiónCaída aparente
first_visit → add_to_cart7.796 → 7.63497,92 %2,08 %
add_to_cart → select_item7.634 → 7.58299,32 %0,68 %
select_item → begin_checkout7.582 → 7.20895,07 %4,93 %
begin_checkout → add_payment_info7.208 → 6.25086,71 %13,29 %
add_payment_info → purchase6.250 → 6.24099,84 %0,16 %

La razón final es 6.240 / 7.796 = 80,04 %. La mayor caída aparente ocurre de begin_checkout a add_payment_info: 13,29 %, con una diferencia de 958 en los conteos.

La consulta compara usuarios distintos por evento; no verifica que sean los mismos usuarios ni que hayan seguido el orden temporal en una sesión. Las tasas se interpretan como ratios agregados, no como un funnel secuencial validado.
Ver código y resultados documentados

Celda 69 · código documentado

# PARTE 1: Totales del funnel
# ======================

# PARTE 1: TOTALES DEL FUNNEL
# =================================

query_totals = '''

-- Seleccionamos el nombre de cada tipo de evento
SELECT nombre_evento,

-- Contamos usuarios únicos que realizaron cada evento
COUNT(DISTINCT id_usuario) AS usuarios

-- Indicamos que los datos se obtienen de la tabla events
FROM events

-- Agrupamos los resultados por tipo de evento
GROUP BY nombre_evento

-- Ordenamos las etapas por cantidad de usuarios, de mayor a menor
ORDER BY usuarios DESC;

'''

# Ejecutamos la consulta SQL y guardamos el resultado en un DataFrame
totals = pd.read_sql(query_totals, con=engine)

# Mostramos la tabla con los totales de usuarios por etapa
totals
nombre_evento usuarios
0 first_visit 7796
1 add_to_cart 7634
2 select_item 7582
3 begin_checkout 7208
4 add_payment_info 6250
5 purchase 6240

Celda 71 · código documentado

# PARTE 2: ANALISIS DE CONVERSION Y DROP-OFF
# =================================

query_conversion = '''

-- Contamos los usuarios únicos de cada evento
WITH cte_eventos AS (

    -- Agrupamos por el nombre de cada evento
    SELECT
        nombre_evento,

        -- Contamos usuarios únicos que realizaron cada evento
        COUNT(DISTINCT id_usuario) AS usuarios

    -- Usamos la tabla events
    FROM events

    -- Creamos un total para cada nombre de evento
    GROUP BY nombre_evento
)

-- Calculamos las tasas entre las etapas del funnel
SELECT

    -- Conversión de first_visit a add_to_cart
    ROUND(
        (
            SELECT usuarios
            FROM cte_eventos
            WHERE nombre_evento = 'add_to_cart'
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'first_visit'
            ),
            0
        ),
        2
    ) AS conversion_first_visit_add_to_cart,

    -- Drop-off de first_visit a add_to_cart
    ROUND(
        (
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'first_visit'
            )
            -
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'add_to_cart'
            )
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'first_visit'
            ),
            0
        ),
        2
    ) AS dropoff_first_visit_add_to_cart,

    -- Conversión de add_to_cart a select_item
    ROUND(
        (
            SELECT usuarios
            FROM cte_eventos
            WHERE nombre_evento = 'select_item'
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'add_to_cart'
            ),
            0
        ),
        2
    ) AS conversion_add_to_cart_select_item,

    -- Drop-off de add_to_cart a select_item
    ROUND(
        (
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'add_to_cart'
            )
            -
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'select_item'
            )
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'add_to_cart'
            ),
            0
        ),
        2
    ) AS dropoff_add_to_cart_select_item,

    -- Conversión de select_item a begin_checkout
    ROUND(
        (
            SELECT usuarios
            FROM cte_eventos
            WHERE nombre_evento = 'begin_checkout'
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'select_item'
            ),
            0
        ),
        2
    ) AS conversion_select_item_begin_checkout,

    -- Drop-off de select_item a begin_checkout
    ROUND(
        (
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'select_item'
            )
            -
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'begin_checkout'
            )
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'select_item'
            ),
            0
        ),
        2
    ) AS dropoff_select_item_begin_checkout,

    -- Conversión de begin_checkout a add_payment_info
    ROUND(
        (
            SELECT usuarios
            FROM cte_eventos
            WHERE nombre_evento = 'add_payment_info'
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'begin_checkout'
            ),
            0
        ),
        2
    ) AS conversion_begin_checkout_add_payment_info,

    -- Drop-off de begin_checkout a add_payment_info
    ROUND(
        (
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'begin_checkout'
            )
            -
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'add_payment_info'
            )
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'begin_checkout'
            ),
            0
        ),
        2
    ) AS dropoff_begin_checkout_add_payment_info,

    -- Conversión de add_payment_info a purchase
    ROUND(
        (
            SELECT usuarios
            FROM cte_eventos
            WHERE nombre_evento = 'purchase'
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'add_payment_info'
            ),
            0
        ),
        2
    ) AS conversion_add_payment_info_purchase,

    -- Drop-off de add_payment_info a purchase
    ROUND(
        (
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'add_payment_info'
            )
            -
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'purchase'
            )
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'add_payment_info'
            ),
            0
        ),
        2
    ) AS dropoff_add_payment_info_purchase,

    -- Tasa de conversión final:
    -- usuarios del último paso / usuarios del primer paso × 100
    ROUND(
        (
            SELECT usuarios
            FROM cte_eventos
            WHERE nombre_evento = 'purchase'
        )::numeric
        * 100
        /
        NULLIF(
            (
                SELECT usuarios
                FROM cte_eventos
                WHERE nombre_evento = 'first_visit'
            ),
            0
        ),
        2
    ) AS conversion_final

'''

# Ejecutamos la consulta SQL
conversion = pd.read_sql(query_conversion, con=engine)

# Mostramos únicamente los porcentajes calculados
conversion
conversion_first_visit_add_to_cart dropoff_first_visit_add_to_cart conversion_add_to_cart_select_item dropoff_add_to_cart_select_item conversion_select_item_begin_checkout dropoff_select_item_begin_checkout conversion_begin_checkout_add_payment_info dropoff_begin_checkout_add_payment_info conversion_add_payment_info_purchase dropoff_add_payment_info_purchase conversion_final
0 97.92 2.08 99.32 0.68 95.07 4.93 86.71 13.29 99.84 0.16 80.04
04

Paso 4. Medir retención semanal por cohortes

22 cohortes · 8.000 usuarios · semanas 1–4

Las tablas users y user_activity se unen con LEFT JOIN. DATE_TRUNC define la semana de registro y COUNT(DISTINCT CASE WHEN ...) identifica usuarios activos en ventanas 1–7, 8–14, 15–21 y 22–28 días.

Semana posterior al registroActivos retenidos / 8.000Tasa agregada ponderada
Semana 13.36042,00 %
Semana 23.35541,94 %
Semana 33.35041,88 %
Semana 43.25040,63 %

La tasa agregada de semana 4 es 40,63 %: 3.250 activos sobre 8.000 usuarios de las cohortes. El mapa de calor conserva las tasas originales de cada cohorte, no solo un promedio global.

Esta retención semanal mide actividad de usuarios. La matriz mensual de Power BI mide ingresos atribuidos a cohortes de compra: son poblaciones, unidades y ventanas diferentes.

Ver código y resultados documentados

Celda 82 · código documentado

# RETENCIÓN SEMANAL POR COHORTES
# ===============================

query_cohort_retention_final = '''

-- PASO 1: Identificamos la cohorte semanal de cada usuario
WITH cohortes AS (

    -- Seleccionamos el identificador de cada usuario
    SELECT
        u.id_usuario,

        -- Convertimos la fecha de registro al inicio de su semana
        DATE_TRUNC(
            'week',
            CAST(u.fecha_registro AS DATE)
        ) AS cohorte_semana

    -- Utilizamos la tabla users
    FROM users AS u
),

-- PASO 2: Relacionamos cada usuario con su actividad semanal
actividad_semanal AS (

    -- Seleccionamos la cohorte semanal del usuario
    SELECT
        c.cohorte_semana,

        -- Conservamos el identificador del usuario
        c.id_usuario,

        -- Tomamos la semana transcurrida desde el registro
        ua.dias_despues_registro,

        -- Conservamos si el usuario estuvo activo
        ua.activo

    -- Comenzamos desde las cohortes
    FROM cohortes AS c

    -- Unimos la actividad con el usuario correspondiente
    LEFT JOIN user_activity AS ua
        ON c.id_usuario = ua.id_usuario

),

-- PASO 3: Calculamos los usuarios iniciales y los retenidos
retencion AS (

    -- Agrupamos los resultados por cohorte semanal
    SELECT
        cohorte_semana,

        -- Contamos los usuarios iniciales de cada cohorte
        COUNT(DISTINCT id_usuario) AS clientes_iniciales,

        -- Contamos usuarios activos durante la semana 1
        COUNT(DISTINCT CASE
            WHEN dias_despues_registro BETWEEN 1 AND 7
                 AND activo = 1
            THEN id_usuario
        END) AS retenido_w1,

        -- Contamos usuarios activos durante la semana 2
        COUNT(DISTINCT CASE
            WHEN dias_despues_registro BETWEEN 8 AND 14
                 AND activo = 1
            THEN id_usuario
        END) AS retenido_w2,

        -- Contamos usuarios activos durante la semana 3
        COUNT(DISTINCT CASE
            WHEN dias_despues_registro BETWEEN 15 AND 21
                 AND activo = 1
            THEN id_usuario
        END) AS retenido_w3,

        -- Contamos usuarios activos durante la semana 4
        COUNT(DISTINCT CASE
            WHEN dias_despues_registro BETWEEN 22 AND 28
                 AND activo = 1
            THEN id_usuario
        END) AS retenido_w4


    -- Utilizamos la actividad semanal
    FROM actividad_semanal

    -- Agrupamos por cohorte semanal
    GROUP BY cohorte_semana
)

-- PASO 4: Mostramos la tabla final de retención
SELECT

    -- Mostramos la semana de registro de la cohorte
    TO_CHAR(cohorte_semana, 'YYYY-MM-DD') AS cohorte,

    -- Mostramos los usuarios iniciales
    clientes_iniciales,

    -- Mostramos los usuarios retenidos en cada semana
    retenido_w1,
    retenido_w2,
    retenido_w3,
    retenido_w4,


    -- Calculamos el porcentaje de retención de la semana 1
    ROUND(
        retenido_w1::numeric
        / NULLIF(clientes_iniciales, 0)
        * 100,
        2
    ) AS retencion_w1_pct,

    -- Calculamos el porcentaje de retención de la semana 2
    ROUND(
        retenido_w2::numeric
        / NULLIF(clientes_iniciales, 0)
        * 100,
        2
    ) AS retencion_w2_pct,

    -- Calculamos el porcentaje de retención de la semana 3
    ROUND(
        retenido_w3::numeric
        / NULLIF(clientes_iniciales, 0)
        * 100,
        2
    ) AS retencion_w3_pct,

    -- Calculamos el porcentaje de retención de la semana 4
    ROUND(
        retenido_w4::numeric
        / NULLIF(clientes_iniciales, 0)
        * 100,
        2
    ) AS retencion_w4_pct

-- Utilizamos la tabla de retención
FROM retencion

-- Ordenamos las cohortes desde la más antigua
ORDER BY cohorte_semana;

'''

# Ejecutamos la consulta SQL
cohorte_final = pd.read_sql(
    query_cohort_retention_final,
    con=engine
)

# Mostramos la tabla final de retención semanal
cohorte_final
cohorte clientes_iniciales retenido_w1 retenido_w2 retenido_w3 retenido_w4 retencion_w1_pct retencion_w2_pct retencion_w3_pct retencion_w4_pct
0 2024-12-30 236 99 91 95 97 41.95 38.56 40.25 41.10
1 2025-01-06 351 145 157 148 138 41.31 44.73 42.17 39.32
2 2025-01-13 362 158 138 152 148 43.65 38.12 41.99 40.88
3 2025-01-20 394 174 156 157 159 44.16 39.59 39.85 40.36
4 2025-01-27 373 155 177 149 164 41.55 47.45 39.95 43.97
5 2025-02-03 405 164 177 184 168 40.49 43.70 45.43 41.48
6 2025-02-10 364 153 145 164 137 42.03 39.84 45.05 37.64
7 2025-02-17 338 159 136 135 138 47.04 40.24 39.94 40.83
8 2025-02-24 353 149 148 151 145 42.21 41.93 42.78 41.08
9 2025-03-03 363 144 158 146 147 39.67 43.53 40.22 40.50
10 2025-03-10 366 154 161 151 144 42.08 43.99 41.26 39.34
11 2025-03-17 394 162 170 175 157 41.12 43.15 44.42 39.85
12 2025-03-24 360 148 145 151 155 41.11 40.28 41.94 43.06
13 2025-03-31 361 146 159 150 146 40.44 44.04 41.55 40.44
14 2025-04-07 379 174 173 166 177 45.91 45.65 43.80 46.70
15 2025-04-14 368 157 164 154 142 42.66 44.57 41.85 38.59
16 2025-04-21 389 154 160 153 151 39.59 41.13 39.33 38.82
17 2025-04-28 379 168 143 150 138 44.33 37.73 39.58 36.41
18 2025-05-05 379 156 151 152 153 41.16 39.84 40.11 40.37
19 2025-05-12 364 133 158 156 148 36.54 43.41 42.86 40.66
20 2025-05-19 389 166 154 169 165 42.67 39.59 43.44 42.42
21 2025-05-26 333 142 134 142 133 42.64 40.24 42.64 39.94
05

Paso 5. Validar la nueva interfaz de checkout

10.000 usuarios · prueba z · p = 0,4160585

El experimento compara Control con Tratamiento. La hipótesis nula plantea igualdad de tasas de compra; la alternativa bilateral plantea una diferencia.

VarianteUsuariosComprasConversión
Control4.96577915,69 %
Tratamiento5.03582016,29 %

Con proportions_ztest(), z = −0,81328 y p = 0,4160585. La diferencia observada es +0,60 puntos porcentuales a favor de Tratamiento, pero no se rechaza H₀ con α = 0,05.

No hay evidencia suficiente para adoptar la UI por una mejora de conversión. El contraste no demuestra equivalencia exacta ni ausencia de cualquier efecto.
Ver código y resultados documentados

Celda 90 · código documentado

# Número de usuarios convertidos por página
conversiones = experiment_checkout_ui.groupby('variante')['convirtio'].sum()
totales = experiment_checkout_ui.groupby('variante')['convirtio'].count()

# Total de usuarios por página
conteos = [conversiones['control'], conversiones['tratamiento']]
num_observaciones = [totales['control'], totales['tratamiento']]

print("Usuarios convertidos por UI:\n", conteos)
print("\nTotal de usuarios por UI:\n", num_observaciones)
Usuarios convertidos por UI:
 [779, 820]

Total de usuarios por UI:
 [4965, 5035]

Celda 91 · código documentado

# Aplicar prueba
from statsmodels.stats.proportion import proportions_ztest


# Ejecutar el z-test para visualizar resultados
z_stat, p_value = proportions_ztest(conteos, num_observaciones)
print(f"Estadístico z: {z_stat:.5f}")
print(f"Valor P: {p_value:.7f}")

alpha = 0.05  # umbral de significancia
if p_value < alpha:
    print("Rechazamos la hipótesis nula: hay evidencia de una diferencia.")
else:
    print("No rechazamos la hipótesis nula: no hay evidencia suficiente de una diferencia.")

#---------------------------------------------------------------------------------------------
# Obtener tasa o porcentaje de éxito por grupo
tasa_A = conteos[0] / num_observaciones[0]
tasa_B = conteos[1] / num_observaciones[1]

print(f"\nTasa UI Control: {tasa_A:.2%}")
print(f"Tasa UI Tratamiento: {tasa_B:.2%}")
print(f"Diferencia: {tasa_B - tasa_A:.2%}")
Estadístico z: -0.81328
Valor P: 0.4160585
No rechazamos la hipótesis nula: no hay evidencia suficiente de una diferencia.

Tasa UI Control: 15.69%
Tasa UI Tratamiento: 16.29%
Diferencia: 0.60%
06

Paso 6. Comunicar el resultado en Power BI

Overview ejecutivo · detalle por producto · cohortes monetarias

Los CSV limpios alimentan el informe BI. Overview reúne revenue, costos, profit, marketing, ticket, cantidad media, evolución por país y categoría. La vista de detalle baja al producto y a pedidos con pérdidas.

La tabla de catálogo documenta costos y proveedores, y el drill-through permite investigar un producto en Power BI. La matriz mensual agrega revenue por cohorte; no es el mapa semanal de actividad calculado en SQL.

El detalle incluye operaciones de Laptop-Gaming-16GB por 10.000 unidades con profit negativo. Son focos concretos para revisar precio, costo, descuentos y consistencia de cantidades.

07

Entrega final. Entrega del proyecto final

Python + SQL + estadística + BI en una presentación

La documentación reúne 102 celdas de notebook, seis figuras originales de Python/SQL y dos páginas exportadas del dashboard. El repositorio incluye notebook, PBIX y los tres CSV limpios.

Esta página integra el proceso completo y mantiene las salidas originales. Las descargas incorporadas permiten consultar el notebook y el PDF; el archivo Power BI está enlazado al final.

Las credenciales de conexión se excluyen de la copia pública descargable y se sustituyen por una variable de entorno, sin cambiar las consultas ni sus resultados guardados.

Hallazgos y prioridades de negocio

Las métricas financieras, conductuales y experimentales convergen en focos concretos de auditoría e investigación, sin asumir causas ausentes en los datos.

El resultado es positivo, pero el costo domina
Evidencia

Revenue $51.836.375,14; productos $43.078.678,80; marketing identificado $2.694.664,43.

Lectura

El costo de productos absorbe aproximadamente 83,11 % del revenue. El resultado medido tras marketing es $6.063.031,91, con margen 11,70 %.

Prioridad

Revisar costo unitario, descuentos y órdenes negativas. Incorporar gastos excluidos y otros costos antes de concluir utilidad neta total.

La laptop concentra volumen y expone pérdidas
Evidencia

Laptop-Gaming-16GB: 144.160 unidades. El detalle muestra pedidos por 10.000 unidades, incluido uno con −$2.375.400 de profit antes de marketing.

Lectura

Hay concentración y operaciones extremas. La consistencia aritmética del monto no certifica que cantidad o precio sean comercialmente correctos.

Prioridad

Auditar precios, costos, cantidades y posibles errores de carga. No atribuir subsidios de membresía sin ingresos de suscripción y reglas comerciales.

Información de pago es el principal foco del funnel
Evidencia

Ratio begin_checkout → add_payment_info: 86,71 %; caída aparente de 13,29 %.

Lectura

Ese tramo presenta la mayor diferencia relativa entre conteos de eventos, pero no revela por sí solo por qué ocurre.

Prioridad

Validar secuencia por usuario/sesión y revisar métodos de pago, mensajes, errores y costos mostrados en checkout.

La actividad semanal se mantiene cerca del 40 %
Evidencia

22 cohortes de registro; tasas agregadas 42,00 %, 41,94 %, 41,88 % y 40,63 % en semanas 1–4.

Lectura

El patrón es de actividad semanal; no mide abandono permanente, compras continuas ni la recurrencia monetaria mensual del dashboard.

Prioridad

Comparar segmentos y horizontes completos. Diseñar hipótesis de reactivación y verificar si generan actividad incremental.

La UI nueva no demuestra mejora de conversión
Evidencia

Control 15,69 %; Tratamiento 16,29 %; diferencia +0,60 pp. z = −0,81328; p = 0,4160585.

Lectura

La diferencia observada no es suficiente para rechazar igualdad de tasas al nivel 0,05.

Prioridad

No adoptar el cambio únicamente por conversión. Definir efecto mínimo relevante, potencia, costos y métricas de experiencia antes de repetir el experimento.

El ingreso acumulado y las cohortes monetarias requieren contexto
Evidencia

Power BI muestra revenue YTD de $51,84 millones y una matriz de ingresos mensuales por cohorte.

Lectura

Un acumulado creciente no demuestra aceleración; los repuntes de revenue pueden estar afectados por pedidos extremos y el mix de productos.

Prioridad

Separar ventas ordinarias de operaciones excepcionales y comparar tasas o compradores únicos, no solo importes acumulados.

Lectura ejecutiva integrada

Una síntesis que conecta producto, experiencia y medición.

RappiPlus registra $51,84 millones de revenue y un resultado de $6,06 millones después de costos de producto y marketing identificado. La prioridad económica es auditar las operaciones extremas de Laptop-Gaming-16GB y sus pérdidas. En experiencia, el principal foco aparente es el paso a información de pago. La actividad retenida en semana 4 ronda 40,63 %, y la nueva UI no muestra mejora estadísticamente significativa. Las decisiones deben apoyarse en calidad, costos completos y validación de hipótesis.

Alcance y límites de las conclusiones
  • Los KPIs de ventas usan 24.600 pedidos completos; 400 pedidos permanecen auditables. Los resultados no representan todos los registros originales sin una evaluación de exclusión.
  • Marketing_clean excluye 101 registros con canal ausente. Sus importes no entran en el gasto identificado utilizado en el profit de Python; pueden modificar el resultado económico completo.
  • Profit antes y después de marketing no es utilidad neta después de todos los costos. El dataset no incluye el ingreso de suscripciones ni permite probar que estas financien pérdidas de producto.
  • Valores de 10.000 o 20.000 unidades pueden influir fuertemente en medias y revenue. No se descartan automáticamente, pero requieren validación comercial y de origen.
  • El funnel usa usuarios únicos contados por evento de forma independiente. No comprueba orden temporal, pertenencia a una misma sesión ni intersección entre etapas.
  • La retención SQL se define por actividad en semanas posteriores al registro. La matriz de Power BI contiene ingresos por meses de cohorte de compra; no son la misma métrica.
  • Una menor actividad en una semana no implica abandono permanente. Debe revisarse completitud de seguimiento y posibilidad de actividad intermitente.
  • El test A/B no aporta evidencia significativa con p = 0,4160585. Esto no demuestra equivalencia y no sustituye intervalos, análisis de potencia o efecto mínimo relevante.
  • La distribución del presupuesto entre canales no mide retorno, CAC o causalidad de campañas. Las compras extremas pueden distorsionar rankings de países y productos.
  • Los descuentos observados de 0, 5, 10 y 15 son importes en la fórmula de monto_total; no se presentan como porcentajes de descuento.
  • Las exportaciones BI son estáticas. Drill-through y segmentadores se utilizan en el PBIX, no sobre las imágenes de esta web.
  • El contenido utiliza salidas guardadas y cálculos descriptivos verificables; no reejecuta la conexión SQL ni añade pruebas nuevas. La copia descargable sustituye la contraseña por una variable de entorno.

Visualizaciones del proyecto completo

Seis figuras originales del notebook y nueve vistas del informe Power BI: dos páginas completas y siete recortes de sus gráficos y tablas. El panel de ventas contiene cuatro gráficos adicionales dentro de la misma figura.

Las figuras del notebook se conservan intactas. Las vistas Power BI se reproducen desde el PDF sin recalcular valores. La galería permite filtrar, ampliar y aplicar zoom; los filtros sobre los datos y el drill-through se utilizan en el PBIX.
15 de 15 visuales
Visual 01 / 15 · Notebook · celda 56

Rentabilidad del negocio

Revenue, producto, marketing y resultado final

Figura original de barras con revenue, costo de producto, gasto de marketing identificado y resultado posterior a ambos costos.

El profit de esta figura resta marketing, a diferencia de la tarjeta de Power BI.

Visual 02 / 15 · Notebook · celda 57

Ventas, ingresos y marketing

Cuatro gráficos: unidades, revenue, canales y países

Panel original con top de unidades, top de ingresos por producto, inversión por canal y comparación de países. Laptop-Gaming-16GB domina el volumen y concentra facturación.

La etiqueta “Top 5 países” conserva el código original; la base limpia solo contiene Argentina, Colombia y México.

Visual 03 / 15 · Notebook · celda 58

Matriz de dispersión de pedidos

Muestra de 1.500 pedidos · random_state = 42

Pairplot original de cantidad, precio unitario, descuento e ingreso por pedido. Las cantidades extremas afectan la escala y la interpretación de medias.

Es una muestra de la base de 24.600 pedidos, no todas las observaciones. La asociación visual no implica causalidad.

Visual 04 / 15 · Notebook · celda 73

Ratios de conversión por transición

86,71 %: checkout → información de pago

Barras originales con las cinco razones de conversión calculadas por SQL a partir de usuarios distintos por evento.

Las razones agregadas no garantizan secuencia temporal o que los conjuntos de usuarios sean los mismos.

Visual 05 / 15 · Notebook · celda 74

Caída aparente entre eventos

13,29 %: mayor drop-off relativo

Figura original de porcentajes de caída, complementarios a los ratios de conversión. El tramo de información de pago destaca como foco de revisión.

El gráfico localiza una diferencia de conteos, no una causa probada de deserción.

Visual 06 / 15 · Notebook · celda 83

Retención semanal de registro

22 cohortes · cuatro ventanas semanales

Mapa de calor original con porcentajes por cohorte de registro. Las tasas agregadas ponderadas se sitúan alrededor de 42 % y llegan a 40,63 % en semana 4.

Mide actividad semanal de usuarios; no la matriz mensual de ingresos que aparece en Power BI.

Visual 07 / 15 · Power BI · página 1 del PDF

Power BI: Overview completo

Revenue $51,84 M · profit antes de marketing $8,76 M

Reproducción de la primera página del PDF: indicadores, evolución mensual por país, acumulado de revenue, categorías y catálogo de producto/proveedor.

Los segmentadores son parte de la exportación estática. El profit de BI no resta marketing.

Visual 08 / 15 · Power BI · página 2 del PDF

Power BI: Detalle completo

Producto, pedidos con pérdidas y cohortes de ingresos

Reproducción de la segunda página del informe. Integra volumen por producto, margen, tabla detallada de órdenes y matriz monetaria por cohorte.

Drill-through y filtros se utilizan en el PBIX; la matriz mide importes, no tasas de retención.

Visual 09 / 15 · Power BI · página 1 del PDF

Profit mensual por país

Argentina, Colombia y México

Recorte del informe con la evolución mensual del profit antes de marketing. La narrativa del dashboard destaca un repunte de Argentina y periodos negativos en otros mercados.

Los movimientos requieren revisar composición y pedidos extremos antes de atribuir causas comerciales.

Visual 10 / 15 · Power BI · página 1 del PDF

Revenue acumulado del periodo

YTD: $51.836.375 en el contexto presentado

Serie acumulada de revenue. Resume el aporte mensual a la facturación del periodo representado.

Un acumulado creciente no prueba aceleración del negocio; debe analizarse el incremento de cada mes.

Visual 11 / 15 · Power BI · página 1 del PDF

Categorías: volumen económico y margen

Electrónica ≈ $45,52 millones de facturación rotulada

Comparación de las categorías Electrónica, Hogar y Moda. La electrónica concentra la facturación y presenta menor margen relativo que las otras categorías en el informe.

La etiqueta original “% de Ganacias” se conserva en la imagen; no demuestra utilidad neta.

Visual 12 / 15 · Power BI · página 1 del PDF

Catálogo: producto, costo y proveedor

Siete referencias comerciales

Tabla de costos unitarios y proveedores que contextualiza el cálculo económico por producto y el análisis de detalle.

El costo de catálogo es una referencia del dataset; no incluye todos los costos logísticos u operativos.

Visual 13 / 15 · Power BI · página 2 del PDF

Unidades y margen por producto

Laptop: 144.160 unidades · margen mostrado 6,81 %

Visual del detalle que compara unidades y margen porcentual por producto. La laptop concentra el volumen, pero su margen difiere del de otras referencias.

Descuentos de membresía o ventas mayoristas son hipótesis; no están demostrados por este gráfico.

Visual 14 / 15 · Power BI · página 2 del PDF

Órdenes con pérdidas y detalle monetario

Una orden de laptop muestra −$2.375.400 de profit

Tabla de pedidos con cantidades, precios, revenue, costo y profit. Se observan operaciones de 10.000 unidades y precios inferiores al costo unitario.

Las operaciones deben auditarse antes de concluir subsidios, abuso o errores de plataforma.

Visual 15 / 15 · Power BI · página 2 del PDF

Cohortes mensuales de revenue

Importes de compra por meses desde la cohorte

Matriz de ingresos con seis cohortes mensuales de 2025 y posiciones 0–5. Permite revisar cuándo se atribuye revenue a cada cohorte.

No mide retención de usuarios. Los importes altos en meses posteriores pueden estar influidos por órdenes extremas y diferente seguimiento.

Archivos del proyecto final

El notebook documenta el análisis, el PDF conserva la presentación BI y el PBIX permite explorar el informe. El repositorio reúne además los CSV limpios.

Proyecto Final · RappiPlus: de datos a decisiones de negocio

S12_Estudiante_Proyecto_Final.ipynb
Andrés Bahamon · TripleTen Data Analytics · Sprint 12

Notebook de consulta

Incluye limpieza, KPIs, SQL, visualizaciones y el contraste A/B. La copia incorporada conserva resultados y sustituye la contraseña de conexión por una variable de entorno.

Informe PDF

Exportación de las dos páginas del dashboard. La descarga al inicio entrega el PDF completo incorporado en este HTML.

Archivo Power BI

El PBIX contiene las vistas, segmentadores y navegación a detalle de producto. Está enlazado a Google Drive.

Fuentes: notebook adjunto S12-Estudiante_Proyecto_Final.ipynb, salidas guardadas de sus 102 celdas e informe Proyecto_Final_Andres_Bahamon-5.pdf. Las agregaciones ponderadas de retención y participaciones de unidades se derivan de conteos documentados; no se incorporan nuevas pruebas ni se conecta a la base SQL. Las imágenes Power BI son reproducciones del PDF. Fecha de preparación: 6 de octubre de 2026.

Visual del proyecto

Visual ampliado

↑