Clasificación
En resumen: Una clasificación predice una clase, por ejemplo «volverá a comprar» o no; la regresión logística da una probabilidad, y el umbral cambia precisión por exhaustividad según qué errores cuesten más.
Problema
«¿Volverá a comprar el cliente?»
Marketing quiere mandar un cupón de descuento a los clientes que de otro modo dejarían de comprar. Un cupón para todos cuesta mucho, y la mayoría de los clientes vuelve de todos modos. La pregunta es: ¿a quién?
Volvamos atrás en el tiempo. Ponte en el 1 de julio de 2026: es la fecha de corte. De cada cliente conoces sus pedidos hasta esa fecha. Cuántos, hace cuántos días fue el último, de cuánto es la cesta media. Si vuelve a pedir lo sabes de julio a finales de octubre:
Antes del 1 de julio, 1279 clientes compraron algo. Después, el 72,6 % de ellos volvió a pedir. La columna lost es 1 para el resto: no pidieron nada durante 4 meses. Para esas personas es el cupón.
Más precisamente, para curiosos
Aquí contamos todos los pedidos, también los cancelados: la pregunta es si el cliente viene, no cuántos ingresos aporta. basket es el valor medio de su cesta. ~people.index.isin(...) es la máscara «el cliente falta después de la fecha de corte»: ~ invierte True y False.
Pruébalo
¿Cuánto sale gratis?
Guarda en lost_share qué proporción de los clientes está perdida, y en baseline_acc con qué frecuencia acierta la regla «todos vuelven», sin ningún modelo.
Solución
import pandas as pd
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['value'] = items['quantity'] * items['unit_price']
orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
lost_share = people['lost'].mean()
baseline_acc = (people['lost'] == 0).mean()
print(round(lost_share, 3), round(baseline_acc, 3))people['lost'].mean() da 0,274: el 27,4 % está perdido. La regla «todos vuelven» falla justo con ellos y acierta con el resto: 1 − 0,274 = 0,726. Esta es la línea base de la lección sobre el flujo de trabajo. Cualquier modelo para esta pregunta tiene que estar por encima del 72,6 %, o no lo necesitas.
La idea
Clasificación: la respuesta es una clase
En la regresión la predicción es un número: los ingresos de un día. Aquí la respuesta es «sí» o «no»: ¿está el cliente perdido? Una tarea así es una clasificación, y las respuestas posibles son clases. El camino es el mismo: características X, etiqueta y, división, fit, predict.
El primer modelo es la regresión logística (LogisticRegression). El nombre engaña: clasifica. Como la regresión lineal, suma las características con pesos, pero luego comprime la suma hasta una probabilidad entre 0 y 1. Frente a ella ponemos DummyClassifier, que siempre dice la clase más frecuente:
La regla «todos vuelven» acierta con el 72,5 % de los clientes de prueba. El modelo acierta con el 72,2 %: menos que la regla. La exactitud (la proporción de respuestas correctas) engaña aquí. Cuando más o menos uno de cada cuatro clientes está perdido, «todos se quedan» casi siempre es verdad.
Más precisamente, para curiosos
stratify=y divide de modo que la proporción de clientes perdidos sea la misma en el entrenamiento y en la prueba: 27,5 %. Sin él, con clases desiguales, la prueba puede recibir por casualidad más o menos clientes perdidos. Aquí es justo dividir a los clientes al azar: las características de cada cliente son de antes de la fecha de corte, y la etiqueta es de después. max_iter=1000 le da al modelo más pasos para encontrar los pesos: las características tienen escalas distintas (pedidos hasta 77, días hasta 240).
La idea
La matriz de confusión
La exactitud es un número para todos los errores. Pero los errores no son iguales. Un cliente perdido que se escapa es un ingreso perdido. Un cupón para un cliente que habría vuelto es un descuento regalado. La tabla que los cuenta por separado se llama matriz de confusión:
Las filas son la verdad, las columnas lo que dice el modelo. De 88 clientes perdidos el modelo señala a 1. Los otros 87 se le escapan. Por eso es tan exacto como la regla: también él casi siempre dice «se queda».
La idea
Precisión y exhaustividad: quién recibe un cupón
Dos números describen los cupones mejor que la exactitud:
- precisión: de los clientes con cupón, cuántos realmente se habrían ido. Baja significa muchos descuentos regalados;
- exhaustividad: de todos los que se habrían ido, cuántos reciben un cupón. Baja significa muchos clientes que se escapan.
El modelo da 3 cupones. Uno va a un cliente perdido: la precisión es 0,333. De los 88 perdidos atrapa 1: la exhaustividad es 0,011. Un modelo así no ayuda a marketing, sea cual sea su exactitud.
La idea
La probabilidad y el umbral
predict no es todo lo que sabe el modelo. predict_proba da una probabilidad para cada clase. Las columnas siguen el orden de model.classes_: la primera es para 0 (se queda), la segunda para 1 (perdido):
La mayor probabilidad de pérdida en la prueba es 0,56. predict da un cupón solo si la probabilidad supera 0,5, y hay 3 clientes así. Pero 0,5 no es una ley. El número a partir del cual dices «sí» se llama umbral, y lo eliges tú. Con un umbral de 0,3 hay 160 cupones:
De 160 cupones, 63 van a clientes perdidos: la precisión es del 39,4 %. De los 88 perdidos atrapas 63: la exhaustividad es del 71,6 %. La exactitud baja, pero ahora el modelo funciona. Como comparación: 160 cupones a clientes al azar tendrían una precisión igual a la proporción de perdidos, 27,5 %. El modelo ordena a los clientes mejor que el azar, aunque lejos de ser perfecto.
Más precisamente, para curiosos
Otro modelo da un cuadro parecido. Un árbol de decisión de profundidad 3 también es exactamente tan exacto como la regla, y con un umbral de 0,3 tiene una precisión del 38,8 % y una exhaustividad del 64,8 %:
Con estas tres características no se puede sacar mucho más. Si quieres un modelo mejor, busca características que digan más del cliente: qué productos compra, con qué regularidad pide.
Pruébalo
Cuéntalos tú
Con un umbral de 0,4, cuenta fp (un cupón para un cliente que se queda) y fn (un cliente perdido sin cupón), y luego calcula la precisión y la exhaustividad a partir de tp, fp y fn, sin sklearn.
Solución
import pandas as pd
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['value'] = items['quantity'] * items['unit_price']
orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
X = people[['orders', 'days_since', 'basket']]
y = people['lost']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y)
model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
proba = model.predict_proba(X_test)[:, 1]
pred = (proba >= 0.4).astype(int)
truth = y_test.to_numpy()
tp = ((pred == 1) & (truth == 1)).sum()
fp = ((pred == 1) & (truth == 0)).sum()
fn = ((pred == 0) & (truth == 1)).sum()
precision = tp / (tp + fp)
recall = tp / (tp + fn)
print(tp, fp, fn, round(precision, 3), round(recall, 3))Con un umbral de 0,4, tp = 37, fp = 44 y fn = 51. Precisión = 37 / (37 + 44) = 0,457: divides entre todos los que tienen cupón. Exhaustividad = 37 / (37 + 51) = 0,42: divides entre todos los clientes perdidos. Si las intercambias, respondes la otra pregunta.
La idea
El umbral cambia una por otra
Cada umbral da cupones distintos. Aquí están todos los umbrales de 0,1 a 0,5:
Con un umbral de 0,1 hay 262 cupones y atrapas al 96,6 % de los clientes perdidos, pero la precisión es del 32,4 %: un poco por encima del 27,5 % de los cupones al azar. Con 0,4 hay 81 cupones y la precisión es del 45,7 %, pero se te escapan más de la mitad de los clientes perdidos. Con 0,5 solo hay 3 cupones. Ningún umbral es el mejor por sí solo. La elección depende del coste: cuánto cuesta un cupón y cuánto cuesta un cliente perdido.
Ejercicio
Los cupones de marketing
Marketing tiene 80 cupones para los clientes de la prueba. Dalos a los 80 con mayor probabilidad de pérdida: proba.argsort() da las posiciones de la menor a la mayor probabilidad. Guarda en hits cuántos de ellos están realmente perdidos, en precision su proporción entre los 80, y en recall qué proporción de todos los clientes perdidos de la prueba recibe un cupón.
Solución
import pandas as pd
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['value'] = items['quantity'] * items['unit_price']
orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
X = people[['orders', 'days_since', 'basket']]
y = people['lost']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y)
model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
proba = model.predict_proba(X_test)[:, 1]
top = proba.argsort()[::-1][:80]
hits = y_test.to_numpy()[top].sum()
precision = hits / 80
recall = hits / y_test.sum()
print(hits, round(precision, 3), round(recall, 3))proba.argsort()[::-1][:80] da las posiciones de los 80 con mayor probabilidad. De ellos, 36 están perdidos: precisión 0,45, exhaustividad 36 / 88 = 0,409. 80 cupones al azar habrían encontrado a unos 22 clientes perdidos. Con argsort() sin invertir, das los cupones a los clientes más seguros, y con la columna [:, 0] ordenas por la probabilidad de que el cliente se quede.
Marketing dice: «Queremos llegar al menos al 80 % de los clientes perdidos». Encuentra en best el umbral más alto de 0,05 a 0,5 en el que la exhaustividad es al menos 0,8, y en best_precision la precisión en él.
Solución
import pandas as pd
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['value'] = items['quantity'] * items['unit_price']
orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
X = people[['orders', 'days_since', 'basket']]
y = people['lost']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y)
model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
proba = model.predict_proba(X_test)[:, 1]
import numpy as np
from sklearn.metrics import precision_score, recall_score
best = None
for t in np.arange(0.05, 0.51, 0.05).round(2):
pred = (proba >= t).astype(int)
if recall_score(y_test, pred) >= 0.8:
best = t
pred = (proba >= best).astype(int)
best_precision = precision_score(y_test, pred)
print(best, round(best_precision, 3))La exhaustividad es al menos 0,8 hasta un umbral de 0,25. Hay 193 cupones, la exhaustividad es del 86,4 % y la precisión del 39,4 %. Un umbral más bajo también llega al 80 %, pero con más cupones innecesarios. Por eso buscas el umbral más alto que aún cumple el objetivo.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Fechas y series temporales · El primer gráfico · Modelos: aprender de ejemplos · Regresión lineal
- Continúa en: Fuga de datos · Validación cruzada
- Ejercicios sobre el tema: Modelos con scikit-learn