Análisis de cohortes de clientes de una tienda online¶

Paso 1. Acceda los datos y prepáralos para el análisis¶

In [2]:
# Importar Librerías

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime, timedelta
from scipy import stats
import plotly.express as px
In [3]:
visits = pd.read_csv('visits_log_us.csv')
orders = pd.read_csv('orders_log_us.csv')
costs = pd.read_csv('costs_us.csv')
In [4]:
# Normalizar nombres de columnas

visits.columns = visits.columns.str.strip().str.lower().str.replace(" ", "_")
orders.columns = orders.columns.str.strip().str.lower().str.replace(" ", "_")
costs.columns  = costs.columns.str.strip().str.lower().str.replace(" ", "_")
In [5]:
# Asegúrate de que cada columna contenga el tipo de datos correcto.

# Comprobación de tipos antes de procesar

print("\n=== INFO ANTES DE PROCESAR ===\n")
print("visits:", visits.info())
print("orders:", orders.info())
print("costs:", costs.info())
=== INFO ANTES DE PROCESAR ===

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 359400 entries, 0 to 359399
Data columns (total 5 columns):
 #   Column     Non-Null Count   Dtype 
---  ------     --------------   ----- 
 0   device     359400 non-null  object
 1   end_ts     359400 non-null  object
 2   source_id  359400 non-null  int64 
 3   start_ts   359400 non-null  object
 4   uid        359400 non-null  uint64
dtypes: int64(1), object(3), uint64(1)
memory usage: 13.7+ MB
visits: None
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50415 entries, 0 to 50414
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   buy_ts   50415 non-null  object 
 1   revenue  50415 non-null  float64
 2   uid      50415 non-null  uint64 
dtypes: float64(1), object(1), uint64(1)
memory usage: 1.2+ MB
orders: None
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2542 entries, 0 to 2541
Data columns (total 3 columns):
 #   Column     Non-Null Count  Dtype  
---  ------     --------------  -----  
 0   source_id  2542 non-null   int64  
 1   dt         2542 non-null   object 
 2   costs      2542 non-null   float64
dtypes: float64(1), int64(1), object(1)
memory usage: 59.7+ KB
costs: None
In [6]:
# Corrección de tipos de fecha/hora necesarios

# costs: columna de fecha
costs['dt'] = pd.to_datetime(costs['dt'], errors='coerce')

# orders: timestamp de compra
orders['buy_ts'] = pd.to_datetime(orders['buy_ts'], errors='coerce')

# visits: timestamps de inicio y fin de la visita
visits['start_ts'] = pd.to_datetime(visits['start_ts'], errors='coerce')
visits['end_ts']   = pd.to_datetime(visits['end_ts'], errors='coerce')
In [7]:
# === Comprobar valores faltantes por columna ===

print("=== Faltantes en visits ===")
print(visits.isna().sum(), "\n")

print("=== Faltantes en orders ===")
print(orders.isna().sum(), "\n")

print("=== Faltantes en costs ===")
print(costs.isna().sum(), "\n")

# === Comprobar número total de duplicados ===

print("=== Duplicados en visits ===")
print(visits.duplicated().sum(), "\n")

print("=== Duplicados en orders ===")
print(orders.duplicated().sum(), "\n")

print("=== Duplicados en costs ===")
print(costs.duplicated().sum(), "\n")

# Comprobación de tipos después de procesar

print("\n=== INFO DESPUÉS DE PROCESAR ===\n")

print("visits.info():")
print(visits.info(), "\n")

print("orders.info():")
print(orders.info(), "\n")

print("costs.info():")
print(costs.info(), "\n")
=== Faltantes en visits ===
device       0
end_ts       0
source_id    0
start_ts     0
uid          0
dtype: int64 

=== Faltantes en orders ===
buy_ts     0
revenue    0
uid        0
dtype: int64 

=== Faltantes en costs ===
source_id    0
dt           0
costs        0
dtype: int64 

=== Duplicados en visits ===
0 

=== Duplicados en orders ===
0 

=== Duplicados en costs ===
0 


=== INFO DESPUÉS DE PROCESAR ===

visits.info():
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 359400 entries, 0 to 359399
Data columns (total 5 columns):
 #   Column     Non-Null Count   Dtype         
---  ------     --------------   -----         
 0   device     359400 non-null  object        
 1   end_ts     359400 non-null  datetime64[ns]
 2   source_id  359400 non-null  int64         
 3   start_ts   359400 non-null  datetime64[ns]
 4   uid        359400 non-null  uint64        
dtypes: datetime64[ns](2), int64(1), object(1), uint64(1)
memory usage: 13.7+ MB
None 

orders.info():
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50415 entries, 0 to 50414
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype         
---  ------   --------------  -----         
 0   buy_ts   50415 non-null  datetime64[ns]
 1   revenue  50415 non-null  float64       
 2   uid      50415 non-null  uint64        
dtypes: datetime64[ns](1), float64(1), uint64(1)
memory usage: 1.2 MB
None 

costs.info():
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2542 entries, 0 to 2541
Data columns (total 3 columns):
 #   Column     Non-Null Count  Dtype         
---  ------     --------------  -----         
 0   source_id  2542 non-null   int64         
 1   dt         2542 non-null   datetime64[ns]
 2   costs      2542 non-null   float64       
dtypes: datetime64[ns](1), float64(1), int64(1)
memory usage: 59.7 KB
None 

Paso 2. Haz informes y calcula métricas¶

Visitas:

In [8]:
# ¿Cuántas personas lo usan cada día, semana y mes?

visits['session_year'] = visits['start_ts'].dt.isocalendar().year
visits['session_month'] = visits['start_ts'].dt.month
visits['session_week']  = visits['start_ts'].dt.isocalendar().week
visits['session_date'] = visits['start_ts'].dt.date

dau_total = visits.groupby('session_date').agg({'uid': 'nunique'})
wau_total = visits.groupby(['session_year', 'session_week']).agg({'uid': 'nunique'})
mau_total = visits.groupby(['session_year', 'session_month']).agg({'uid': 'nunique'})

wau_index = wau_total.index.to_frame(index=False)

wau_index['session_week'] = wau_index['session_week'].astype(int)

year_str_w = wau_index['session_year'].astype(str)
week_str_w = wau_index['session_week'].astype(str).str.zfill(2)
iso_str_w  = year_str_w + '-W' + week_str_w + '-1'

wau_dates = pd.to_datetime(iso_str_w, format='%G-W%V-%u', errors='coerce')

wau_total.index = wau_dates

mau_index = mau_total.index.to_frame(index=False)

mau_index['session_month'] = mau_index['session_month'].astype(int)

year_str_m  = mau_index['session_year'].astype(str)
month_str_m = mau_index['session_month'].astype(str).str.zfill(2)
date_str_m  = year_str_m + '-' + month_str_m + '-01'

mau_dates = pd.to_datetime(date_str_m, format='%Y-%m-%d', errors='coerce')

mau_total.index = mau_dates

fig, axes = plt.subplots(3, 1, figsize=(12, 15))

# 1. DAU - Usuarios Activos Diarios
axes[0].plot(dau_total.index, dau_total['uid'], linewidth=2)
axes[0].set_title('Usuarios Activos Diarios (DAU)', fontsize=14, fontweight='bold')
axes[0].set_xlabel('Fecha')
axes[0].set_ylabel('Número de Usuarios Únicos')
axes[0].grid(True, alpha=0.3)
axes[0].tick_params(axis='x', rotation=45)

# 2. WAU - Usuarios Activos Semanales
axes[1].plot(wau_total.index, wau_total['uid'], linewidth=2)
axes[1].set_title('Usuarios Activos Semanales (WAU)', fontsize=14, fontweight='bold')
axes[1].set_xlabel('Semana')
axes[1].set_ylabel('Número de Usuarios Únicos')
axes[1].grid(True, alpha=0.3)
axes[1].tick_params(axis='x', rotation=45)

# 3. MAU - Usuarios Activos Mensuales
axes[2].plot(mau_total.index, mau_total['uid'], linewidth=2)
axes[2].set_title('Usuarios Activos Mensuales (MAU)', fontsize=14, fontweight='bold')
axes[2].set_xlabel('Mes')
axes[2].set_ylabel('Número de Usuarios Únicos')
axes[2].grid(True, alpha=0.3)
axes[2].tick_params(axis='x', rotation=45)

plt.tight_layout()
plt.show()
No description has been provided for this image
In [9]:
# ¿Cuántas sesiones hay por día? (Un usuario puede tener más de una sesión).

sesiones_diarias = visits.groupby('session_date').size()

print(f"Sesiones diarias promedio: {sesiones_diarias.mean():.0f}")
print(f"DAU promedio: {int(dau_total.mean())}")
Sesiones diarias promedio: 987
DAU promedio: 907
/var/folders/nn/8fxj8t5x4w3fy9hxxr41r9sw0000gn/T/ipykernel_7566/621433215.py:6: FutureWarning: Calling int on a single element Series is deprecated and will raise a TypeError in the future. Use int(ser.iloc[0]) instead
  print(f"DAU promedio: {int(dau_total.mean())}")
In [10]:
# ¿Cuál es la duración de cada sesión?

visits['session_duration_sec'] = (visits['end_ts'] - visits['start_ts']).dt.total_seconds()

visits['session_duration_min'] = visits['session_duration_sec'] / 60

duracion_promedio = visits['session_duration_min'].mean()

plt.figure(figsize=(12, 6))
plt.hist(visits['session_duration_min'], bins=50, alpha=0.7, color='skyblue', edgecolor='black')
plt.title('Distribución de Duración de Sesiones', fontsize=16, fontweight='bold')
plt.xlabel('Duración (minutos)', fontsize=12)
plt.ylabel('Número de Sesiones', fontsize=12)

plt.axvline(duracion_promedio, color='red', linestyle='--', linewidth=2, label=f'Duración Promedio: {duracion_promedio:.1f} min')

plt.legend(fontsize=20, loc='upper right', frameon=True, fancybox=True, shadow=True)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
No description has been provided for this image
In [11]:
# ¿Con qué frecuencia los usuarios regresan?
visits_with_duration = visits[visits['session_duration_sec'] > 0]

sesiones_por_usuario = visits_with_duration.groupby('uid').size()

total_usuarios = sesiones_por_usuario.count()
usuarios_unicos = (sesiones_por_usuario == 1).sum()
usuarios_recurrentes = total_usuarios - usuarios_unicos

print(f"Total de usuarios: {total_usuarios:,}")
print(f"Usuarios únicos (1 sesión): {usuarios_unicos:,} ({usuarios_unicos/total_usuarios*100:.1f}%)")
print(f"Usuarios recurrentes (2+ sesiones): {usuarios_recurrentes:,} ({usuarios_recurrentes/total_usuarios*100:.1f}%)")
Total de usuarios: 207,051
Usuarios únicos (1 sesión): 160,499 (77.5%)
Usuarios recurrentes (2+ sesiones): 46,552 (22.5%)

Ventas:

In [12]:
# ¿Cuándo empieza la gente a comprar?

first_visit = visits.groupby('uid')['start_ts'].min().reset_index()
first_visit.columns = ['uid', 'first_visit_date']

first_order = orders.groupby('uid')['buy_ts'].min().reset_index()
first_order.columns = ['uid', 'first_order_date']

conversion_data = first_visit.merge(first_order, on='uid', how='inner')
conversion_data['conversion_time'] = conversion_data['first_order_date'] - conversion_data['first_visit_date']
conversion_data['days_to_convert'] = conversion_data['conversion_time'].dt.days

conversion_data['conversion_group'] = np.where(
    conversion_data['days_to_convert'] == 0, 'Conversión inmediata',
    np.where(
        (conversion_data['days_to_convert'] >= 1) & (conversion_data['days_to_convert'] <= 7), 'Conversión Rapida',
        np.where(
            (conversion_data['days_to_convert'] >= 8) & (conversion_data['days_to_convert'] <= 30), 'Conversión Media',
            'Conversión Lenta'
        )
    )
)

conversion_groups = conversion_data.groupby('conversion_group')['uid'].count()

order = ['Conversión inmediata', 'Conversión Rapida', 'Conversión Media', 'Conversión Lenta']
conversion_groups = conversion_groups.reindex(order)

group_ranges = {
    'Conversión inmediata': '(0 días)',
    'Conversión Rapida': '(1-7 días)', 
    'Conversión Media': '(8-30 días)',
    'Conversión Lenta': '(31+ días)'
}

labels_with_ranges = [f"{group} {group_ranges[group]}" for group in conversion_groups.index]

avg_conversion_time = conversion_data['days_to_convert'].mean()
print(f"Tiempo promedio entre el primer registro y la primera compra: {avg_conversion_time:.1f} días")

first_visit_with_source = visits.merge(
    first_visit,
    left_on=['uid', 'start_ts'],
    right_on=['uid', 'first_visit_date'],
    how='inner'
)[['uid', 'source_id']]

conversion_analysis = conversion_data.merge(first_visit_with_source, on='uid', how='left')

conversions_by_source = conversion_analysis.groupby('source_id').agg({
    'uid': 'count',        
    'days_to_convert': 'mean'  
}).round(1)

conversions_by_source.columns = ['total_conversions', 'avg_days_to_convert']
conversions_by_source = conversions_by_source.sort_values('total_conversions', ascending=False)

top_sources = conversions_by_source.head(8)
other_conversions = conversions_by_source.iloc[8:]['total_conversions'].sum()

if other_conversions > 0:
    pie_data = list(top_sources['total_conversions']) + [other_conversions]
    pie_labels = [f'Fuente {idx}' for idx in top_sources.index] + ['Otras fuentes']
else:
    pie_data = list(top_sources['total_conversions'])
    pie_labels = [f'Fuente {idx}' for idx in top_sources.index]

colors = plt.cm.Set3(np.linspace(0, 1, len(pie_data)))
explode = [0.05 if i < 3 else 0 for i in range(len(pie_data))] 

total_conversions = conversions_by_source['total_conversions'].sum()
top3_pct = top_sources.head(3)['total_conversions'].sum() / total_conversions * 100
print(f"\nTotal de conversiones: {total_conversions:,}")
print(f"Top 3 fuentes representan: {top3_pct:.1f}% del total")

fig, axes = plt.subplots(1, 2, figsize=(18, 8))

bars = axes[0].bar(
    range(len(conversion_groups)),
    conversion_groups.values,
    color=['#2E8B57', '#4682B4', '#DAA520', '#CD5C5C']
)

axes[0].set_title('Comparación de Tiempos de Conversión', fontsize=16, fontweight='bold')
axes[0].set_xlabel('Grupo de Conversión', fontsize=12)
axes[0].set_ylabel('Número de Usuarios', fontsize=12)

axes[0].set_xticks(range(len(conversion_groups)))
axes[0].set_xticklabels(labels_with_ranges, rotation=45, ha='right')

for i, bar in enumerate(bars):
    height = bar.get_height()
    axes[0].text(
        bar.get_x() + bar.get_width()/2., height + 200,
        f'{int(height):,} usuarios',
        ha='center', va='bottom',
        fontsize=10, fontweight='bold'
    )

axes[0].grid(True, alpha=0.3, axis='y')



wedges, texts, autotexts = axes[1].pie(
    pie_data,
    labels=pie_labels,
    autopct='%1.1f%%',
    startangle=90,
    colors=colors,
    explode=explode
)

for autotext in autotexts:
    autotext.set_color('black')
    autotext.set_fontweight('bold')
    autotext.set_fontsize(10)

axes[1].set_title(
    'Distribución de Conversiones por Fuente de Anuncios',
    fontsize=16, fontweight='bold', pad=20
)

axes[1].axis('equal')

plt.tight_layout()
plt.show()
Tiempo promedio entre el primer registro y la primera compra: 16.7 días

Total de conversiones: 36,523
Top 3 fuentes representan: 75.8% del total
No description has been provided for this image
In [13]:
# ¿Cuántos pedidos hacen durante un período de tiempo dado?

orders_per_month = orders.groupby(orders['buy_ts'].dt.to_period('M'))['uid'].count()

plt.figure(figsize=(12, 6))
plt.plot(orders_per_month.index.astype(str), orders_per_month.values, 
         marker='o', linewidth=2, markersize=8)
plt.title('Pedidos por Mes', fontsize=16, fontweight='bold')
plt.xlabel('Mes', fontsize=12)
plt.ylabel('Número de Pedidos', fontsize=12)
plt.xticks(rotation=45)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

print(f"Promedio mensual de pedidos: {orders_per_month.mean():.0f}")
print(f"Mes con más pedidos: {orders_per_month.idxmax()} ({orders_per_month.max():,} pedidos)")
No description has been provided for this image
Promedio mensual de pedidos: 3878
Mes con más pedidos: 2017-12 (6,218 pedidos)
In [14]:
# ¿Cuál es el tamaño promedio de compra?

avg_revenue_per_order = orders.groupby(orders['buy_ts'].dt.to_period('M'))['revenue'].mean()

plt.figure(figsize=(12, 6))
plt.plot(avg_revenue_per_order.index.astype(str), avg_revenue_per_order.values, 
         marker='o', linewidth=2, markersize=8, color='green')
plt.title('Tamaño Promedio de Compra', fontsize=16, fontweight='bold')
plt.ylabel('Revenue Promedio ($)', fontsize=12)
plt.xticks(rotation=45)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
No description has been provided for this image
In [15]:
# ¿Cuánto dinero traen? (LTV)

ltv_por_usuario = orders.groupby('uid').agg(
    ltv=('revenue', 'sum'),
    compras=('revenue', 'count'),
    ticket_promedio=('revenue', 'mean')
).sort_values('ltv', ascending=False)

ltv_promedio = ltv_por_usuario['ltv'].mean()
print(f"LTV promedio: ${ltv_promedio:,.2f}")

ltv_con_cohorte = orders.merge(first_visit, on='uid', how='left')
ltv_con_cohorte['cohorte'] = ltv_con_cohorte['first_visit_date'].dt.to_period('M')

ltv_por_cohorte = (
    ltv_con_cohorte
    .groupby('cohorte')['revenue']
    .sum()
    .sort_index()
)

cohort_dates = ltv_por_cohorte.index.to_timestamp()

plt.figure(figsize=(12, 6))

plt.plot(
    cohort_dates,
    ltv_por_cohorte.values,
    linewidth=3,
    marker='o',
    markersize=8,
)

for x, y in zip(cohort_dates, ltv_por_cohorte.values):
    etiqueta = f"${y:,.0f}"       
    plt.text(
        x,
        y + (y * 0.02),           
        etiqueta,
        ha='center', va='bottom',
        fontsize=9
    )

plt.title('LTV por Cohorte de Adquisición', fontsize=14, fontweight='bold')
plt.xlabel('Mes de Cohorte')
plt.ylabel('Ingresos Acumulados')
plt.grid(alpha=0.3)
plt.xticks(rotation=45)

plt.show()
LTV promedio: $6.90
No description has been provided for this image

Marketing:

In [16]:
# ¿Cuánto dinero se gastó?  (Total/por fuente de adquisición/a lo largo del tiempo) 
gasto_total = costs['costs'].sum()
print(f"Gasto total en marketing: ${gasto_total:,.2f}")

# Gasto por fuente de marketing
gasto_por_fuente = costs.groupby('source_id')['costs'].sum().sort_values(ascending=False)

# Gasto por mes
gasto_por_mes = costs.groupby(costs['dt'].dt.to_period('M'))['costs'].sum()

gasto_por_mes.index = gasto_por_mes.index.to_timestamp()

fig, axes = plt.subplots(1, 2, figsize=(18, 6))

axes[0].pie(
    gasto_por_fuente,
    labels=gasto_por_fuente.index,
    autopct='%1.1f%%',
    startangle=90,
    counterclock=False
)
axes[0].set_title('Gasto por Fuente de Marketing', fontsize=14, fontweight='bold')

axes[1].bar(
    gasto_por_mes.index.strftime('%Y-%m'),
    gasto_por_mes.values
)
axes[1].set_title('Gasto Mensual en Marketing', fontsize=14, fontweight='bold')
axes[1].set_xlabel('Mes')
axes[1].set_ylabel('Gasto ($)')
axes[1].tick_params(axis='x', rotation=45)
axes[1].grid(axis='y', alpha=0.3)

plt.tight_layout()
plt.show()
Gasto total en marketing: $329,131.62
No description has been provided for this image
In [17]:
# ¿Cuál fue el costo de adquisición de clientes de cada una de las fuentes?

first_source = (
    visits.sort_values('start_ts')
    .groupby('uid', as_index=False)['source_id']
    .first()
)

buyers = orders[['uid']].drop_duplicates()

buyers_with_source = buyers.merge(first_source, on='uid', how='left')

clientes_por_fuente = (
    buyers_with_source.groupby('source_id')['uid']
    .nunique()
    .rename('n_clientes')
)

gasto_por_fuente = costs.groupby('source_id')['costs'].sum()

cac_por_fuente = gasto_por_fuente.to_frame().join(clientes_por_fuente, how='left')

cac_por_fuente['cac'] = cac_por_fuente['costs'] / cac_por_fuente['n_clientes']

fig, ax = plt.subplots(figsize=(10, 6))

ax.bar(cac_por_fuente.index.astype(str), cac_por_fuente['cac'])

ax.set_title('Costo de Adquisición de Clientes (CAC) por Fuente', fontsize=14, fontweight='bold')
ax.set_xlabel('Fuente de adquisición (source_id)')
ax.set_ylabel('CAC ($ por cliente)')
ax.grid(axis='y', alpha=0.3)
plt.xticks(rotation=45)

for i, v in enumerate(cac_por_fuente['cac']):
    ax.text(i, v + (v * 0.01), f'${v:.2f}', ha='center', va='bottom')
    
plt.tight_layout()
plt.show()
No description has been provided for this image
In [18]:
# ¿Cuán rentables eran las inversiones? (ROMI)

ingresos_por_usuario = orders.groupby('uid')['revenue'].sum().reset_index()

ingresos_con_fuente = ingresos_por_usuario.merge(first_source, on='uid', how='left')

ingresos_por_fuente = ingresos_con_fuente.groupby('source_id')['revenue'].sum()

costo_por_fuente = costs.groupby('source_id')['costs'].sum()

romi = (ingresos_por_fuente - costo_por_fuente) / costo_por_fuente
romi_df = romi.to_frame(name='romi').sort_index()

plt.figure(figsize=(10, 6))

plt.bar(romi_df.index.astype(str), romi_df['romi'], color='skyblue')

plt.title('ROMI por Fuente de Adquisición', fontsize=14, fontweight='bold')
plt.xlabel('Fuente (source_id)')
plt.ylabel('ROMI (retorno por peso invertido)')
plt.grid(axis='y', alpha=0.3)
plt.xticks(rotation=45)

plt.axhline(0, color='red', linewidth=2)

plt.tight_layout()
plt.show()

resumen_romi = pd.DataFrame({
    'Ingresos': ingresos_por_fuente,
    'Costos': costo_por_fuente,
    'ROMI': romi
}).fillna(0)

resumen_romi['Rentable'] = resumen_romi['ROMI'] > 0
print(resumen_romi.round(2))
No description has been provided for this image
           Ingresos     Costos  ROMI  Rentable
source_id                                     
1          31090.55   20833.27  0.49      True
2          46923.61   42806.04  0.10      True
3          54511.24  141321.63 -0.61     False
4          56696.83   61073.60 -0.07     False
5          52624.02   51757.10  0.02      True
7              1.22       0.00  0.00     False
9           5759.40    5517.49  0.04      True
10          4450.33    5822.49 -0.24     False

Paso 3. Escribe una conclusión: aconseja a los expertos de marketing cuánto dinero invertir y dónde¶

Después de analizar el desempeño de cada fuente de adquisición a partir de las métricas de Ingresos, Costos, y especialmente ROMI, observamos un patrón claro: solo algunas fuentes generan resultados positivos contribuyendo a la adquisición de clientes, mientras que otras generan retornos despreciables o directamente no son rentables.

Fuentes recomendables para invertir según el analisis:

  1. Fuente 1:

    • ROMI = 0.49 (49% de retorno)
    • Ingresos: 31,090
    • Costos: 20,833

Es la fuente más rentable. Por cada peso invertido devuelve casi medio peso adicional. Además, su relación ingreso/costo es estable y supera por mucho a todas las demás.

  1. Fuente 2:
    • ROMI = 0.10 (10% de retorno)
    • Ingresos: 46,923
    • Costos: 42,806

Ligero retorno positivo, apenas por encima del punto de equilibrio. Aunque no es explosivamente rentable, no está destruyendo valor, lo cual la vuelve una fuente segura con potencial de optimización.

  1. Fuente 9:
    • ROMI = 0.04 (4% de retorno)
    • Ingresos: 5,759
    • Costos: 5,517

Solo genera un pequeño retorno, pero no genera pérdida, por lo que es una fuente estable de bajo riesgo.

  1. Fuente 5
    • ROMI = 0.02 (2% de retorno)
    • Ingresos: 52,624
    • Costos: 51,757

Genera ingresos casi iguales a su costo. Aunque la rentabilidad es baja, no genera pérdidas, por lo que puede mantenerse si se mejora el targeting o el formato de campaña.

Fuentes no recomendables para invertir según el analisis:

  1. Fuente 3
    • ROMI = –0.61
    • Ingresos: 54,511
    • Costos: 141,321

La peor fuente del portafolio. Por cada 1 peso invertido se pierde más de medio peso.

  1. Fuente 10
    • ROMI = –0.24
    • Ingresos: 4,450
    • Costos: 5,822

Pequeño volumen, pero mal desempeño.

  1. Fuente 4
    • ROMI = –0.07
    • Ingresos: 56,696
    • Costos: 61,073

No es tan perjudicial como la fuente 3, pero aún es negativa.

En conclusión, el análisis de ROMI, ingresos y costos por fuente muestra que solo algunas plataformas generan verdadero retorno y deben recibir la mayor parte del presupuesto de marketing. Las fuentes 1 y 2 destacan como las más rentables, con retornos positivos que justifican aumentar la inversión en ellas, mientras que las fuentes 5 y 9 muestran resultados ligeramente positivos y pueden mantenerse con vigilancia, buscando optimización para mejorar su desempeño. En contraste, las fuentes 3, 4 y 10 presentan ROMI negativo y están destruyendo valor, por lo que se recomienda reducir drásticamente o pausar la inversión hasta corregir fallas en segmentación o eficiencia. Finalmente, la fuente 7 no aporta valor medible y no debe recibir presupuesto adicional hasta entender su rol. Esta estrategia asegura que los recursos se concentren en las fuentes que realmente impulsan crecimiento y rentabilidad, evitando pérdidas y maximizando el retorno total de las campañas.