Catorce capítulos y llegamos a lo único que le importa a quien pagó por esto: a quién llamo el lunes 📞
Este capítulo es el proyecto entero seguido, sin desviarse. Cada decisión lleva al lado el capítulo donde la discutimos, para que puedas volver.
Y termina donde tiene que terminar un proyecto de datos: en una decisión de negocio con su precio medido al lado.
El encargo
El equipo comercial puede visitar 200 clientes al trimestre. Hoy los elige por costumbre. La pregunta es si podemos elegirlos mejor.
Fíjate que el encargo no dice "hazme un modelo". Dice 200. Ese número es el que manda, y es el que va a decidir todo lo que viene 🎯
Antes de tocar nada, tres preguntas al que encarga, que son las del capítulo 2 aplicadas aquí:
- 🎯 ¿Qué se va a hacer distinto? Se cambia el criterio con el que se arma la ruta de visitas. Si la respuesta hubiera sido "nada, es para tener el dato", no hay proyecto.
- 📅 ¿Cuándo se decide? Al empezar el trimestre, así que solo vale lo que se sabe antes de la visita. Eso deja fuera media tabla.
- 💰 ¿Contra qué se compara el éxito? Contra elegir por costumbre. Como no tenemos registro de la costumbre, usamos el azar como listón, que es más exigente de lo que parece.
Y una cuarta que casi nadie hace y ahorra disgustos: ¿qué pasa con los que no entran en la lista? Porque la respuesta por defecto es "no se les visita", y eso tiene consecuencias que vamos a ver en el paso 6 y que no salen en ninguna métrica 👀
Paso 1. Cargar y limpiar
Del capítulo 4, tal cual, sin cambiar nada.
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import GroupShuffleSplit, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'
MARGEN = 0.25 # el negocio se queda con 25 centavos de cada sol vendido
CUPO = 200 # visitas que caben en el trimestre
def carga_limpia(url):
v = pd.read_csv(url).drop_duplicates()
v['ciudad'] = (v['ciudad'].str.strip().str.lower()
.str.normalize('NFKD')
.str.encode('ascii', 'ignore').str.decode('utf-8'))
v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))
for col in ['fecha', 'fecha_ultima_compra']:
f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')
falta = f.isna() & v[col].notna()
f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')
v[col] = f
return v
crudo = pd.read_csv(URL)
datos = carga_limpia(URL)
print('filas en el CSV:', len(crudo))
print('filas después de quitar duplicadas:', len(datos))
print('montos que se habrían perdido con to_numeric a secas: S/',
round(pd.to_numeric(crudo['monto'], errors='coerce').isna().sum() and
datos.loc[pd.to_numeric(crudo['monto'], errors='coerce').reindex(datos.index).isna(),
'monto'].sum(), 2))
filas en el CSV: 3037 filas después de quitar duplicadas: 3000 montos que se habrían perdido con to_numeric a secas: S/ 471534.21
Treinta y siete filas duplicadas fuera y casi medio millón de soles rescatados de la coma decimal. Eso ya justifica el capítulo 4 entero 💰
Paso 2. Las variables, y sobre todo las que no
Del capítulo 9 vienen las banderas de nulo, que era el hallazgo bueno. Del capítulo 12 viene la disciplina de no meter nada que no exista el día que predices.
def prepara(v):
v = v.sort_values(['cliente_id', 'fecha']).copy()
v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)
v['sin_descuento'] = v['descuento'].isna().astype(int)
v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int)
v['precio_unitario'] = v['monto'] / v['unidades']
v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1
return v
NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',
'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero']
CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']
COLS = NUMERICAS + CATEGORICAS
datos = prepara(datos)
print('columnas que entran:', len(COLS))
print()
print('la bandera que valía la pena:')
print(datos.groupby('sin_compra_previa')['compro'].agg(['size', 'mean']).round(4).to_string())
columnas que entran: 13
la bandera que valía la pena:
size mean
sin_compra_previa
0 2456 0.6140
1 544 0.4136
Veinte puntos de diferencia entre las filas donde falta la fecha de la última compra y las demás. El nulo tenía información y por eso se marca antes de rellenarlo.
Y lo que dejamos fuera a propósito: monto_final_facturado, que en
el capítulo 12 daba 0,9994 de AUC y en producción predecía cero de 750 porque no
existe todavía. La lista de columnas prohibidas es tan importante como la de
columnas usadas 🚫
Paso 3. Qué pregunta estoy respondiendo
Aquí es donde la mayoría de los proyectos se tuercen, y es una sola decisión: cómo partir los datos.
En el capítulo 16 usamos GroupShuffleSplit para que un cliente no estuviera en los dos lados. En el capítulo 23 partimos por fecha. Las dos están
bien, porque responden preguntas distintas:
gs = GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=42)
i, j = next(gs.split(datos[COLS], datos['compro'], groups=datos['cliente_id']))
def arma_modelo():
return Pipeline([
('pre', ColumnTransformer([
('num', Pipeline([('r', SimpleImputer(strategy='median')),
('e', StandardScaler())]), NUMERICAS),
('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),
('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS),
])),
('mod', LogisticRegression(max_iter=1000, random_state=42)),
])
por_cliente = arma_modelo().fit(datos[COLS].iloc[i], datos['compro'].iloc[i])
print('cliente que nunca vi :', round(roc_auc_score(
datos['compro'].iloc[j], por_cliente.predict_proba(datos[COLS].iloc[j])[:, 1]), 4))
CORTE = pd.Timestamp('2026-01-01')
pasado = datos[datos['fecha'] < CORTE]
futuro = datos[datos['fecha'] >= CORTE].copy()
modelo = arma_modelo().fit(pasado[COLS], pasado['compro'])
futuro['prob'] = modelo.predict_proba(futuro[COLS])[:, 1]
print('próximo trimestre :', round(roc_auc_score(futuro['compro'], futuro['prob']), 4))
print('clientes en los dos lados de la partición por fecha:',
len(set(pasado['cliente_id']) & set(futuro['cliente_id'])))
cliente que nunca vi : 0.6843 próximo trimestre : 0.7227 clientes en los dos lados de la partición por fecha: 453
0,6843 contra 0,7227. Casi cuatro puntos de diferencia por elegir una partición en vez de la otra.
Y no es que una mienta. La de grupos dice qué tan bien le va con un cliente que nunca vio. La de fecha dice qué tan bien le va el próximo trimestre con la cartera que ya tiene, donde 453 clientes aparecen a los dos lados porque son los mismos de siempre.
El encargo era priorizar la cartera actual. Entonces la partición correcta es la de fecha, y el 0,7227 es el número honesto para este encargo. Si el encargo fuera captar clientes nuevos, el número honesto sería 0,6843 y habría que decirlo así 🧭
Paso 4. El modelo, y contra qué se compara
Del capítulo 13: probamos ocho y ganó la regresión logística. Del capítulo 14: un modelo se compara contra la tontería más simple que exista, no contra el cero.
base = futuro['compro'].mean()
print('si llamo a todos, compra el:', round(base, 4))
print('AUC del modelo :', round(roc_auc_score(futuro['compro'], futuro['prob']), 4))
print()
orden = futuro.sort_values('prob', ascending=False)
print('de los 200 mejores según el modelo, compran:',
int(orden.head(CUPO)['compro'].sum()))
print('de 200 al azar, comprarían :', round(CUPO * base, 1))
si llamo a todos, compra el: 0.5706 AUC del modelo : 0.7227 de los 200 mejores según el modelo, compran: 163 de 200 al azar, comprarían : 114.1
163 contra 114. Cuarenta y nueve ventas más con el mismo esfuerzo, y eso es lo que hay que decir en la reunión, no el 0,7227 😊
Paso 5. Ordenar por lo que de verdad importa
Aquí hay un error que yo cometí durante años: ordenar por probabilidad.
Una bodega con 90% de probabilidad y S/200 de pedido vale menos que un mayorista con 60% y S/3.000. Lo que se ordena es el valor esperado, que es la probabilidad multiplicada por lo que hay en juego:
futuro['valor_esperado'] = futuro['prob'] * futuro['monto'] * MARGEN
por_prob = futuro.nlargest(CUPO, 'prob')
por_valor = futuro.nlargest(CUPO, 'valor_esperado')
for nombre, lista in [('ordenando por probabilidad', por_prob),
('ordenando por valor esperado', por_valor)]:
ventas = int(lista['compro'].sum())
margen = lista.loc[lista['compro'] == 1, 'monto'].sum() * MARGEN
print(f'{nombre:30} {ventas} ventas S/{margen:,.2f} de margen')
ordenando por probabilidad 163 ventas S/56,859.33 de margen ordenando por valor esperado 147 ventas S/76,302.41 de margen
Ordenar por valor esperado consigue menos ventas (147 contra 163) y bastante más plata (S/76.302 contra S/56.859).
Dieciséis ventas menos y S/19.443 más. Si tu jefe mide ventas cerradas, entrega la primera lista; si mide margen, la segunda. Pregunta antes de entregar, porque las dos son defendibles y solo una es la que te pidieron 🤔
Paso 6. El problema de la lista
Vamos a mirar quién está en esa lista tan rentable.
print(por_valor.groupby('segmento').agg(
en_la_lista=('prob', 'size'), compraron=('compro', 'sum')).to_string())
print()
print('bodegas que hubo en el trimestre:', int((futuro['segmento'] == 'Bodega').sum()))
print('de esas, compraron :', int(futuro.loc[futuro['segmento'] == 'Bodega', 'compro'].sum()))
en_la_lista compraron segmento Horeca 8 4 Mayorista 192 143 bodegas que hubo en el trimestre: 237 de esas, compraron : 86
192 mayoristas, 8 de horeca y cero bodegas. Ninguna. En todo el trimestre hubo 237 bodegas y 86 de ellas compraron, y el modelo no manda a visitar ni una 😬
Esto no es un fallo técnico. El modelo hace exactamente lo que le pedimos: maximizar plata. Las bodegas compran menos y compran más barato, así que nunca ganan una comparación por valor esperado.
Pero si esa lista se ejecuta cuatro trimestres seguidos, el negocio se queda sin canal de bodegas, y eso no aparece en ninguna métrica del capítulo 14 📉
Paso 7. Cuánto cuesta no abandonar a nadie
La solución del capítulo 22 aplicada aquí es un cupo por segmento: 50 y 50 y 50 y 50 en vez de 200 al mejor postor.
con_cuota = pd.concat([g.nlargest(50, 'valor_esperado')
for _, g in futuro.groupby('segmento')])
for nombre, lista in [('sin cuota', por_valor), ('con cuota', con_cuota)]:
ventas = int(lista['compro'].sum())
margen = lista.loc[lista['compro'] == 1, 'monto'].sum() * MARGEN
print(f'{nombre}: {ventas} ventas S/{margen:,.2f} de margen')
print()
print(con_cuota.groupby('segmento').agg(
en_la_lista=('prob', 'size'), compraron=('compro', 'sum')).to_string())
sin cuota: 147 ventas S/76,302.41 de margen
con cuota: 132 ventas S/43,432.55 de margen
en_la_lista compraron
segmento
Bodega 50 24
Horeca 50 38
Mayorista 50 41
Minimarket 50 29
La cuota cuesta S/32.870 de margen y 15 ventas en el trimestre. Y a cambio pone 50 bodegas en la lista, de las que 24 compran.
Ese es todo el trabajo, honestamente. No decidir por el negocio: ponerle el precio exacto a las dos opciones y dejar que decida quien responde por ellas 🤝
Y si te preguntan qué harías tú, yo diría la cuota, porque el modelo se entrena con lo que pasó y una lista que nunca visita bodegas deja de generar datos de bodegas. Al año siguiente el modelo va a saber todavía menos de ellas, y esa bola de nieve no se ve venir en ningún tablero.
Paso 8. Lo que se entrega
No se entrega un cuaderno. Se entrega esto:
entrega = con_cuota.sort_values('valor_esperado', ascending=False)[
['cliente_id', 'segmento', 'ciudad', 'canal', 'monto', 'prob', 'valor_esperado']].copy()
entrega['prob'] = entrega['prob'].round(4)
entrega['valor_esperado'] = entrega['valor_esperado'].round(2)
print(entrega.head(10).to_string(index=False))
print()
print('clientes en la lista :', len(entrega))
print('valor esperado total : S/', round(entrega['valor_esperado'].sum(), 2))
cliente_id segmento ciudad canal monto prob valor_esperado
C0284 Mayorista trujillo Web 3481.29 0.9275 807.20
C0056 Mayorista trujillo WhatsApp 3440.83 0.9193 790.82
C0220 Mayorista arequipa WhatsApp 3551.77 0.8102 719.43
C0324 Mayorista arequipa WhatsApp 4175.88 0.6553 684.12
C0426 Mayorista trujillo Tienda 3061.39 0.8859 678.05
C0150 Mayorista lima Marketplace 3149.87 0.8209 646.46
C0224 Mayorista piura Tienda 3162.85 0.8023 634.39
C0363 Mayorista chiclayo Web 2725.01 0.9016 614.22
C0216 Mayorista lima Marketplace 3207.42 0.7646 613.06
C0361 Mayorista lima Tienda 2955.42 0.8221 607.40
clientes en la lista : 200
valor esperado total : S/ 43784.95
Un archivo que el equipo comercial abre y usa, con el nombre del cliente, por qué está ahí y cuánto vale la visita.
Y con eso van tres cosas más, que son las que separan un proyecto de un ejercicio:
- 📄 Una página: qué se predice, con qué datos, qué mide (0,7227), contra qué se compara (114 ventas al azar) y qué no sabe hacer.
- 🚦 La función
revisadel capítulo 23 corriendo antes de cada entrega, para que nadie prediga sobre datos rotos. - 📅 Una fecha para volver a mirar, con el rango de bailoteo ya medido para no asustarse en vano.
Ejercicios
1. El cupo manda más que el modelo
Repite la comparación con cupos distintos y mira dónde deja de valer la pena.
for cupo in [50, 100, 200, 400, 800]:
top = futuro.nlargest(cupo, 'valor_esperado')
margen = top.loc[top['compro'] == 1, 'monto'].sum() * MARGEN
azar = cupo / len(futuro) * futuro.loc[futuro['compro'] == 1, 'monto'].sum() * MARGEN
print(f'cupo={cupo:4d} margen=S/{margen:10,.2f} al azar=S/{azar:10,.2f} '
f'ganancia=S/{margen - azar:9,.2f}')
cupo= 50 margen=S/ 27,912.45 al azar=S/ 6,693.23 ganancia=S/21,219.23 cupo= 100 margen=S/ 47,099.72 al azar=S/ 13,386.46 ganancia=S/33,713.26 cupo= 200 margen=S/ 76,302.41 al azar=S/ 26,772.92 ganancia=S/49,529.49 cupo= 400 margen=S/108,388.92 al azar=S/ 53,545.84 ganancia=S/54,843.09 cupo= 800 margen=S/130,691.38 al azar=S/107,091.67 ganancia=S/23,599.71
La ganancia sube hasta el cupo 400 y después se desinfla, porque cuando visitas a todo el mundo el orden deja de importar.
Esta tabla vale más que cualquier métrica para decidir si contratar más vendedores. El modelo solo sirve cuando no alcanza para todos 📊
2. El margen que no es igual para todos
Hasta aquí usamos 25% parejo. Pon márgenes distintos por categoría y mira si la lista cambia.
MARGENES = {'Abarrotes': 0.18, 'Bebidas': 0.22, 'Cuidado personal': 0.35,
'Limpieza': 0.28, 'Snacks': 0.30}
futuro['ve2'] = futuro['prob'] * futuro['monto'] * futuro['categoria'].map(MARGENES)
otra = futuro.nlargest(CUPO, 've2')
print('coinciden con la lista anterior:',
len(set(otra.index) & set(por_valor.index)), 'de', CUPO)
print()
print(otra.groupby('categoria').size().to_string())
coinciden con la lista anterior: 177 de 200 categoria Abarrotes 32 Bebidas 33 Cuidado personal 48 Limpieza 39 Snacks 48
Coinciden 177 de 200, así que cambiar el margen mueve 23 clientes. Menos de lo que yo esperaba, y es porque el monto pesa mucho más que el margen en la multiplicación.
Y esos márgenes me los inventé yo para el ejercicio. En un proyecto de verdad salen de contabilidad, y conseguirlos suele costar más reuniones que entrenar el modelo entero 📑
3. Cuánto de esto es suerte
Repite la lista con varios cortes de fecha y mira si la ganancia aguanta.
for corte in ['2025-10-01', '2026-01-01', '2026-03-01']:
c = pd.Timestamp(corte)
a, b = datos[datos['fecha'] < c], datos[datos['fecha'] >= c].copy()
m = arma_modelo().fit(a[COLS], a['compro'])
b['prob'] = m.predict_proba(b[COLS])[:, 1]
b['ve'] = b['prob'] * b['monto'] * MARGEN
top = b.nlargest(CUPO, 've')
margen = top.loc[top['compro'] == 1, 'monto'].sum() * MARGEN
azar = CUPO / len(b) * b.loc[b['compro'] == 1, 'monto'].sum() * MARGEN
print(f'{corte} futuro={len(b):4d} ganancia=S/{margen - azar:9,.2f}')
2025-10-01 futuro=1509 ganancia=S/61,836.00 2026-01-01 futuro= 992 ganancia=S/49,529.49 2026-03-01 futuro= 667 ganancia=S/37,016.97
La ganancia sale positiva en los tres cortes y de tamaño parecido. No era la suerte de una fecha 👍
4. La lista sin el segmento
Si el segmento es la columna que ordena casi todo, mira qué lista sale sin él.
SIN = [c for c in COLS if c != 'segmento']
sin_seg = Pipeline([
('pre', ColumnTransformer([
('num', Pipeline([('r', SimpleImputer(strategy='median')),
('e', StandardScaler())]), NUMERICAS),
('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),
('c', OneHotEncoder(handle_unknown='ignore'))]),
['ciudad', 'canal', 'categoria']),
])),
('mod', LogisticRegression(max_iter=1000, random_state=42)),
]).fit(pasado[SIN], pasado['compro'])
futuro['ve3'] = (sin_seg.predict_proba(futuro[SIN])[:, 1] * futuro['monto'] * MARGEN)
otra = futuro.nlargest(CUPO, 've3')
print(otra.groupby('segmento').size().to_string())
print()
print('ventas:', int(otra['compro'].sum()),
' margen: S/', round(otra.loc[otra['compro'] == 1, 'monto'].sum() * MARGEN, 2))
segmento Horeca 9 Mayorista 191 ventas: 145 margen: S/ 75651.94
Sigue sin haber bodegas. Quitar la columna no arregla nada, porque el monto y el resto de columnas ya cuentan quién es mayorista.
Esto es importante y se malentiende mucho: borrar la variable no borra el sesgo. Si quieres que la lista incluya bodegas, la cuota es explícita y funciona; quitar la columna es un gesto que se siente bien y no hace nada 🎭
5. Poner la incertidumbre en la entrega
Marca en la lista los clientes donde el modelo no está seguro, con la predicción conforme del capítulo 22.
X_ent, X_cal, y_ent, y_cal = train_test_split(pasado[COLS], pasado['compro'],
test_size=0.3, random_state=42,
stratify=pasado['compro'])
m_conf = arma_modelo().fit(X_ent, y_ent)
p_cal = m_conf.predict_proba(X_cal)
fallo = 1 - p_cal[np.arange(len(y_cal)), y_cal.values]
k = int(np.ceil((len(fallo) + 1) * 0.80))
liston = 1 - np.sort(fallo)[k - 1]
conj = m_conf.predict_proba(futuro[COLS]) >= liston
futuro['seguro'] = conj.sum(axis=1) == 1
lista = futuro.nlargest(CUPO, 'valor_esperado')
print('en la lista, el modelo está seguro de:', int(lista['seguro'].sum()), 'de', CUPO)
print('aciertan cuando está seguro :',
round(lista.loc[lista['seguro'], 'compro'].mean(), 4))
print('aciertan cuando no lo está :',
round(lista.loc[~lista['seguro'], 'compro'].mean(), 4))
en la lista, el modelo está seguro de: 163 de 200 aciertan cuando está seguro : 0.773 aciertan cuando no lo está : 0.5676
De los 200 de la lista, el modelo está seguro de 163. Y en esos acierta el 77,30%, contra el 56,76% de los 37 donde duda.
Una columna más en el archivo y el vendedor sabe a cuáles llamar primero.
Es de las cosas que más agradecen y menos cuesta poner. Una probabilidad sola invita a creerle; una probabilidad con un "de este no estoy segura" al lado invita a pensar 🧠
6. El error de entregar la lista con el índice de pandas
Intenta guardar la entrega y mira qué sale.
import os import tempfile ruta = os.path.join(tempfile.gettempdir(), 'entrega.csv') entrega.to_csv(ruta) vuelta = pd.read_csv(ruta) print(vuelta.columns.tolist())
['Unnamed: 0', 'cliente_id', 'segmento', 'ciudad', 'canal', 'monto', 'prob', 'valor_esperado']
Ahí está el regalito: Unnamed: 0. El to_csv guardó el
índice de pandas como una columna sin nombre, y el archivo que abre el equipo
comercial empieza con números que nadie sabe qué son.
Y ahora el error de verdad, que es lo que pasa cuando alguien asume que la primera columna es el cliente:
vuelta.iloc[:, 0].str.startswith('C').all()
AttributeError: Can only use .str accessor with string values, not integer
Se arregla con entrega.to_csv(ruta, index=False), y es el último
paso del proyecto, el que nadie revisa, y el que hace que tu trabajo llegue bien o
llegue raro 📤
7. La página que va con el modelo
Genera el resumen que acompaña la entrega, con números y no con adjetivos.
resumen = {
'entrenado con': f"{pasado['fecha'].min().date()} a {pasado['fecha'].max().date()}",
'filas de entrenamiento': len(pasado),
'columnas': len(COLS),
'AUC próximo trimestre': round(roc_auc_score(futuro['compro'], futuro['prob']), 4),
'ventas de la lista': int(por_valor['compro'].sum()),
'ventas al azar': round(CUPO * futuro['compro'].mean(), 1),
'margen de la lista': round(por_valor.loc[por_valor['compro'] == 1, 'monto'].sum() * MARGEN, 2),
'precio de la cuota por segmento': round(
por_valor.loc[por_valor['compro'] == 1, 'monto'].sum() * MARGEN
- con_cuota.loc[con_cuota['compro'] == 1, 'monto'].sum() * MARGEN, 2),
'no sabe hacer': 'clientes nuevos (ahí baja a 0.6843)',
}
for k, v in resumen.items():
print(f'{k:32} {v}')
entrenado con 2025-01-01 a 2025-12-31 filas de entrenamiento 2008 columnas 13 AUC próximo trimestre 0.7227 ventas de la lista 147 ventas al azar 114.1 margen de la lista 76302.41 precio de la cuota por segmento 32869.86 no sabe hacer clientes nuevos (ahí baja a 0.6843)
Nueve líneas. Eso es el proyecto entero contado para alguien que no va a leer tu código nunca.
Si solo te llevas una cosa del libro, que sea esta: el trabajo termina cuando alguien puede tomar una decisión con lo que le diste, no cuando el modelo entrena 💛
Comprueba que lo tienes
El proyecto termina con una lista de 200 clientes. ¿Qué lleva cada fila además del nombre?
- La probabilidad y el valor esperado, para poder ordenar por lo que importa
- Solo la probabilidad, que es lo que el modelo predice
- El segmento y la ciudad
- Nada más: la lista ya está ordenada por el modelo
Lo que te llevas
- 🧭 La partición se elige por la pregunta: 0,7227 para la cartera actual y 0,6843 para clientes nuevos. Las dos son ciertas.
- 📢 A la reunión no se lleva el AUC, se lleva "163 ventas contra 114".
- 💰 Se ordena por valor esperado, no por probabilidad: 16 ventas menos y S/19.443 más de margen.
- 😬 La lista óptima dejó fuera a las 237 bodegas del trimestre, y 86 de ellas compraron.
- 🤝 La cuota por segmento cuesta S/32.870 y 15 ventas. Tu trabajo es poner ese precio sobre la mesa, no decidir.
- 🎭 Quitar la columna del segmento no quita el sesgo: la lista sigue sin bodegas.
- 📤 Se entrega un archivo con nombres, probabilidad y valor, más una página de resumen y una fecha para volver a mirar.
Y hasta aquí el libro. Quince capítulos desde "qué es un modelo" hasta una lista de 200 clientes con su precio al lado 🎓
Si llegaste hasta acá haciendo los ejercicios, ya sabes más de machine learning aplicado que mucha gente que lo pone en el perfil. En serio.
En el capítulo 25 te dejo dónde seguir: la documentación que sí vale la pena, los libros que yo leí y las cosas que este libro no cubre.
Que tengas lindo día! 🌸