Empieza la segunda mitad del libro 🎬
Hasta ahora describíamos lo que teníamos delante. A partir de aquí vamos a afirmar cosas sobre lo que no medimos, y para eso hace falta un poco de probabilidad.
Poca, te lo prometo. Nada de urnas con bolas ni de barajas. Tres ideas, y las tres se calculan con la misma tabla de ventas de siempre.
import pandas as pd
import numpy as np
URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'
def carga_limpia(url):
"""La misma del capítulo 2."""
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
v = carga_limpia(URL)
print('P(compra) = %.4f' % v['compro'].mean())
print('P(mayorista) = %.4f' % (v['segmento'] == 'Mayorista').mean())
P(compra) = 0.5777 P(mayorista) = 0.2500
Ahí tienes la única definición de probabilidad que vas a necesitar: la proporción de veces que pasa algo. Y ya sabes calcularla desde el capítulo 2, porque es la media de una columna de ceros y unos 😌
Regla 1: la suma, con su descuento
¿Cuál es la probabilidad de que una venta sea de un mayorista o venga por WhatsApp?
La tentación es sumar. Vamos a ver:
mayorista = v['segmento'] == 'Mayorista'
whatsapp = v['canal'] == 'WhatsApp'
print('P(mayorista) = %.4f' % mayorista.mean())
print('P(whatsapp) = %.4f' % whatsapp.mean())
print('sumando los dos = %.4f' % (mayorista.mean() + whatsapp.mean()))
print('P(mayorista O whatsapp) = %.4f' % (mayorista | whatsapp).mean())
print('lo que sobra = %.4f' % (mayorista & whatsapp).mean())
P(mayorista) = 0.2500 P(whatsapp) = 0.2397 sumando los dos = 0.4897 P(mayorista O whatsapp) = 0.4327 lo que sobra = 0.0570
Sumando sale 0,4897 y la verdad es 0,4327. Sobran exactamente 0,0570, que es la proporción de ventas que son las dos cosas a la vez: un mayorista que compró por WhatsApp.
Al sumar las contaste dos veces. Por eso la regla es restar la intersección 🧮
sumas las dos probabilidades y le restas lo que contaste dos veces
Regla 2: la condicional, que es la que se usa de verdad
"Dado que ya sé algo, ¿qué probabilidad hay de lo otro". En pandas esto es simplemente filtrar antes de promediar:
de todo lo que cumple B, qué parte cumple también A
print('P(compra) = %.4f' % v['compro'].mean())
print('P(compra | mayorista) = %.4f' % v.loc[mayorista, 'compro'].mean())
print('P(compra | whatsapp) = %.4f' % v.loc[whatsapp, 'compro'].mean())
print('P(compra | marketplace) = %.4f'
% v.loc[v['canal'] == 'Marketplace', 'compro'].mean())
P(compra) = 0.5777 P(compra | mayorista) = 0.7240 P(compra | whatsapp) = 0.6551 P(compra | marketplace) = 0.4613
Saber quién es el cliente cambia la probabilidad: de 0,5777 sube a 0,7240 si es mayorista y baja a 0,4613 si viene por Marketplace 📈
Y eso es, literalmente, de qué va el análisis de datos: buscar el dato que, al conocerlo, cambia lo que esperas 🔮
Regla 3: independencia
Dos cosas son independientes si saber una no te dice nada de la otra. La prueba es esta: si son independientes, la probabilidad de que pasen las dos es el producto de las dos por separado.
Probemos con ciudad, que llevamos seis capítulos sospechando que no sirve para nada:
for c in ['lima', 'arequipa', 'trujillo']:
es_ciudad = v['ciudad'] == c
juntas = (es_ciudad & (v['compro'] == 1)).mean()
producto = es_ciudad.mean() * v['compro'].mean()
print('%-9s juntas %.4f | producto %.4f | diferencia %+.4f'
% (c, juntas, producto, juntas - producto))
lima juntas 0.0917 | producto 0.0907 | diferencia +0.0010 arequipa juntas 0.1047 | producto 0.1051 | diferencia -0.0005 trujillo juntas 0.0930 | producto 0.0907 | diferencia +0.0023
Prácticamente idénticas. Ciudad y compra son independientes: saber de qué ciudad es una venta no te dice nada sobre si se va a cerrar ✅
Y ahora el contraste, con segmento:
juntas = (mayorista & (v['compro'] == 1)).mean()
producto = mayorista.mean() * v['compro'].mean()
print('juntas %.4f' % juntas)
print('producto %.4f' % producto)
print('la realidad es %.1f%% mayor de lo que serIa si fueran independientes'
% (100 * (juntas / producto - 1)))
juntas 0.1810 producto 0.1444 la realidad es 25.3% mayor de lo que serIa si fueran independientes
Un 25,3% por encima. Aquí no hay independencia ninguna: ser mayorista y comprar van juntos 🤝
Guarda este cálculo porque es exactamente el que hace la prueba ji cuadrado del capítulo 11. Lo único que añade la prueba es decirte cuánta diferencia hace falta para que no sea casualidad.
El error que cuesta campañas
Este es el importante del capítulo, y lo he visto en presentaciones de verdad 😖
print('P(compra | mayorista) = %.4f' % v.loc[mayorista, 'compro'].mean())
print('P(mayorista | compra) = %.4f'
% (v.loc[v['compro'] == 1, 'segmento'] == 'Mayorista').mean())
P(compra | mayorista) = 0.7240 P(mayorista | compra) = 0.3133
0,7240 y 0,3133. Son dos números completamente distintos, y en castellano se parecen tanto que se confunden todo el rato:
- ✅ "El 72,40% de los mayoristas compra". Verdad.
- ✅ "El 31,33% de los que compran son mayoristas". Verdad.
- ❌ "El 72,40% de los que compran son mayoristas". Falso, y es lo que la gente entiende.
Y la consecuencia es de plata. Si crees que el 72% de tus compradores son mayoristas, concentras el equipo comercial ahí y abandonas a los demás. Pero resulta que casi siete de cada diez ventas cerradas vienen de otros segmentos 💸
La regla mnemotécnica: P(A dado B) no es P(B dado A). Cuando leas un porcentaje, pregúntate siempre sobre qué total está calculado.
Bayes, que es la fórmula para darle la vuelta
El teorema de Bayes es justo eso: cómo pasar de una a la otra sin equivocarse.
para darle la vuelta a una condicional multiplicas por la tasa base y divides por la del otro suceso
Vamos a comprobar que funciona:
p_may = mayorista.mean()
p_compra = v['compro'].mean()
p_compra_dado_may = v.loc[mayorista, 'compro'].mean()
bayes = p_compra_dado_may * p_may / p_compra
print('con Bayes: %.4f' % bayes)
print('contando: %.4f'
% (v.loc[v['compro'] == 1, 'segmento'] == 'Mayorista').mean())
con Bayes: 0.3133 contando: 0.3133
Clavado 🎯
Fíjate en la pieza que hace todo el trabajo: P(A), la proporción de mayoristas, que es 0,25. Como solo una de cada cuatro ventas es de un mayorista, por muy alta que sea su tasa de compra no pueden ser la mayoría de los cierres.
A esa P(A) se le llama tasa base, y olvidarla es el error más famoso de toda la probabilidad.
La tasa base, con las 21 negativas
Imagina que alguien de sistemas te ofrece un detector de ventas rotas. Dice que acierta el 99%: si la venta está rota la detecta el 99% de las veces, y si está bien solo se equivoca el 1%.
Suena excelente. Vamos a ver qué pasa cuando lo sueltas sobre estas ventas, donde ya sabemos que hay 21 rotas de 3.000:
tasa_base = (v['monto'] < 0).mean()
sensibilidad = 0.99
falsa_alarma = 0.01
alarmas_buenas = sensibilidad * tasa_base
alarmas_falsas = falsa_alarma * (1 - tasa_base)
print('proporcion de ventas rotas: %.5f' % tasa_base)
print('alarmas correctas por venta: %.5f' % alarmas_buenas)
print('alarmas falsas por venta: %.5f' % alarmas_falsas)
print()
print('P(rota | suena la alarma) = %.4f'
% (alarmas_buenas / (alarmas_buenas + alarmas_falsas)))
proporcion de ventas rotas: 0.00700 alarmas correctas por venta: 0.00693 alarmas falsas por venta: 0.00993 P(rota | suena la alarma) = 0.4110
El 41,10%. O sea que cuando el detector del 99% suena, se equivoca más veces de las que acierta 😵
Y no es que el detector sea malo: es buenísimo. El problema es que hay 2.979 ventas sanas y solo 21 rotas. El 1% de errores sobre 2.979 son 29,79 alarmas falsas, más que las 20,79 correctas que salen del 99% sobre 21.
Esto es lo que pasa con todos los detectores de cosas raras: fraude, enfermedades poco frecuentes, fallos de máquina. Cuando lo que buscas es raro, la mayoría de las alarmas son falsas aunque el detector sea buenísimo 🚨
Es también la razón por la que el libro de machine learning tiene un capítulo entero sobre clases raras: el mismo problema, con modelos.
El error del capítulo
v.groupby('segmento')['compro'].mean()['Mayoristas']
KeyError: 'Mayoristas'
Una ese de más 🤦 En el archivo el segmento se llama Mayorista,
en singular.
Parece tonto, y lo pongo aquí porque este error tiene una versión hermana que no da error y que es de las que más daño hacen:
print('mal escrito: %.4f' % v.loc[v['segmento'] == 'Mayoristas', 'compro'].mean())
print('bien escrito: %.4f' % v.loc[v['segmento'] == 'Mayorista', 'compro'].mean())
print('cuantas filas trae el mal escrito:',
(v['segmento'] == 'Mayoristas').sum())
mal escrito: nan bien escrito: 0.7240 cuantas filas trae el mal escrito: 0
Con groupby te avisa. Con un filtro, no: te
devuelve cero filas y un nan, tan tranquilo 😶
Y si en vez de una media hubieras pedido un sum(), te habría
devuelto 0,0, que parece un número de verdad y se cuela en cualquier informe.
La defensa es la de siempre y cuesta un segundo: después de filtrar, mira cuántas filas quedaron.
Practica 💪
1. La probabilidad de compra por canal, ordenada
Calcula P(compra | canal) para los cuatro canales y ordénalos. ¿Cuánto separa al mejor del peor?
por_canal = v.groupby('canal')['compro'].agg(['mean', 'count']).sort_values('mean')
por_canal.columns = ['P(compra)', 'ventas']
print(por_canal.round(4))
print()
print('el mejor supera al peor en %.1f puntos'
% (100 * (por_canal['P(compra)'].max() - por_canal['P(compra)'].min())))
P(compra) ventas canal Marketplace 0.4613 763 Web 0.5808 792 Tienda 0.6198 726 WhatsApp 0.6551 719 el mejor supera al peor en 19.4 puntos
19,4 puntos entre WhatsApp y Marketplace 📱
Y ahí hay una decisión de negocio esperando. Pero cuidado con el salto fácil: esto no dice que mover a la gente a WhatsApp vaya a subir las ventas.
Puede que WhatsApp cierre más porque por ahí escriben los clientes que ya estaban decididos, y Marketplace reciba a los que están mirando precios. En ese caso el canal no causa nada, solo refleja quién ya venía convencido.
Distinguir eso es el capítulo 13, y es el problema más difícil de todo el libro 🧩
2. Un test de independencia casero
Escribe algo que, dada una columna categórica, te diga
cuánto se aleja de la independencia respecto a compro.
def cuanto_depende(df, columna):
p_compra = df['compro'].mean()
peor = 0.0
for valor in df[columna].unique():
es = df[columna] == valor
juntas = (es & (df['compro'] == 1)).mean()
producto = es.mean() * p_compra
peor = max(peor, abs(juntas / producto - 1))
return peor
for col in ['ciudad', 'categoria', 'canal', 'segmento']:
print('%-11s se desvia hasta %5.1f%% de la independencia'
% (col, 100 * cuanto_depende(v, col)))
ciudad se desvia hasta 2.5% de la independencia categoria se desvia hasta 3.2% de la independencia canal se desvia hasta 20.1% de la independencia segmento se desvia hasta 35.1% de la independencia
Ahí está el ranking del archivo entero, en cuatro líneas 🏆
Segmento manda con un 35,1%, canal le sigue con 20,1%, y ciudad y categoría se quedan por debajo del 4%, o sea que son ruido.
Es exactamente lo mismo que veníamos diciendo a ojo desde el capítulo 1, pero ahora con un número que se puede comparar entre columnas.
Lo que este cálculo no te dice es si un 3,4% es poco porque no hay relación o poco porque no hay datos suficientes. Para eso hace falta el capítulo 11 🔬
3. Dale la vuelta tú
Sabemos que P(compra | WhatsApp) es 0,6551. Calcula P(WhatsApp | compra) con Bayes y compruébalo contando.
p_wa = whatsapp.mean()
p_compra = v['compro'].mean()
p_compra_dado_wa = v.loc[whatsapp, 'compro'].mean()
print('P(whatsapp | compra) con Bayes: %.4f'
% (p_compra_dado_wa * p_wa / p_compra))
print('contando: %.4f'
% (v.loc[v['compro'] == 1, 'canal'] == 'WhatsApp').mean())
P(whatsapp | compra) con Bayes: 0.2718 contando: 0.2718
El 65,51% de los que llegan por WhatsApp compran, y solo el 27,18% de las compras llegan por WhatsApp 🔁
Otra vez la tasa base haciendo su trabajo: WhatsApp es solo el 23,97% de las ventas, así que por muy bien que convierta no puede aportar la mayoría de los cierres.
Y de aquí sale una idea de negocio que sí es defendible: si WhatsApp convierte mucho mejor y solo trae una cuarta parte del tráfico, hay margen para mandarle más. Eso es una hipótesis para probar, no una conclusión, pero es del tipo bueno 🌱
4. Un detector con datos de verdad
Usa la satisfacción baja (1 o 2) como alarma de "esta venta no se va a cerrar" y calcula si sirve.
con_dato = v.dropna(subset=['satisfaccion'])
alarma = con_dato['satisfaccion'] <= 2
no_cerro = con_dato['compro'] == 0
print('filas con satisfaccion: %d' % len(con_dato))
print('P(alarma) = %.4f' % alarma.mean())
print('P(no cerro) = %.4f' % no_cerro.mean())
print()
print('P(alarma | no cerro) = %.4f' % alarma[no_cerro].mean())
print('P(alarma | si cerro) = %.4f' % alarma[~no_cerro].mean())
print('P(no cerro | alarma) = %.4f' % no_cerro[alarma].mean())
filas con satisfaccion: 2769 P(alarma) = 0.3962 P(no cerro) = 0.4258 P(alarma | no cerro) = 0.4724 P(alarma | si cerro) = 0.3396 P(no cerro | alarma) = 0.5077
Vamos a leerlo como se lee un detector 🕵️
Caza el 47,24% de las ventas que no se cierran, y se equivoca avisando en el 33,96% de las que sí se cierran. O sea que distingue algo, pero poco: la diferencia entre las dos es de 13 puntos.
Y el número que decide es el último: cuando la alarma suena, acierta el 50,77% de las veces. Prácticamente una moneda al aire 🪙
Compáralo con la tasa base: sin ningún detector, si dijeras "no se va a cerrar" a todo, acertarías el 42,58%. La alarma te sube al 50,77%. Mejora ocho puntos, que no es nada despreciable, pero está lejísimos de "esta venta se cae seguro".
Esa comparación contra la tasa base es lo que hay que exigirle a cualquier alerta antes de montar un proceso encima 📋
5. Fabrica dos cosas independientes
Añade una columna con un número al azar y comprueba que es independiente de la compra. Sirve para ver qué pinta tiene la independencia de verdad.
generador = np.random.default_rng(7)
v['moneda'] = generador.integers(0, 2, size=len(v))
cara = v['moneda'] == 1
juntas = (cara & (v['compro'] == 1)).mean()
producto = cara.mean() * v['compro'].mean()
print('P(cara) = %.4f' % cara.mean())
print('P(compra | cara) = %.4f' % v.loc[cara, 'compro'].mean())
print('P(compra) = %.4f' % v['compro'].mean())
print()
print('juntas %.4f | producto %.4f | se desvia %.2f%%'
% (juntas, producto, 100 * (juntas / producto - 1)))
P(cara) = 0.5033 P(compra | cara) = 0.5755 P(compra) = 0.5777 juntas 0.2897 | producto 0.2908 | se desvia -0.38%
0,38% de desviación 🎲
Y esto es lo valioso del ejercicio: ahora sabes cuánto se desvía algo que es independiente de verdad. Con 3.000 filas, el azar puro produce desviaciones de medio punto porcentual.
Así que cuando ciudad se desvía 2,5%, ya tienes con qué compararlo: es unas seis veces el ruido de una moneda, pero catorce veces menos que segmento.
Fabricar una columna de basura a propósito y medir cuánto se desvía es uno de los trucos más útiles que conozco. Te da la vara de medir del ruido con tus propios datos, sin teoría 📏
6. Probabilidad de dos filtros a la vez
¿Qué probabilidad hay de que una venta sea de un mayorista Y venga por WhatsApp Y se cierre? Calcúlalo directo y con la cadena de condicionales.
directo = (mayorista & whatsapp & (v['compro'] == 1)).mean()
cadena = (mayorista.mean()
* whatsapp[mayorista].mean()
* v.loc[mayorista & whatsapp, 'compro'].mean())
print('directo: %.6f' % directo)
print('cadena: %.6f' % cadena)
print()
print('son %d ventas de %d' % ((mayorista & whatsapp & (v['compro'] == 1)).sum(),
len(v)))
directo: 0.046667 cadena: 0.046667 son 140 ventas de 3000
Las dos formas dan lo mismo, como tenía que ser ✅
La cadena se lee de izquierda a derecha: la probabilidad de ser mayorista, por la de venir por WhatsApp dado que eres mayorista, por la de cerrar dado que eres las dos cosas.
Parece un rodeo cuando puedes contar directo, y lo es aquí. Pero es la forma en que se construyen los cálculos cuando no tienes la tabla entera, solo trozos, y es la base de cómo funcionan los modelos que estiman probabilidades.
Mira también el número de filas: 140. Ese es el aviso de verdad de este ejercicio. Cada condición que añades parte la muestra, y con 140 filas cualquier porcentaje que calcules ya tiene un margen de error gordo. Cuánto de gordo es el capítulo 9 📉
Comprueba que lo tienes
El 72,40% de los mayoristas compra. ¿Qué porcentaje de las compras son de mayoristas?
- El 31,33%, porque los mayoristas son solo una cuarta parte de las ventas
- También el 72,40%
- No se puede saber con esa información
- Más del 72,40%, porque compran más
Lo que te llevas
- 🎲 Una probabilidad es la proporción de veces que pasa algo, o sea la media de una columna de ceros y unos.
- ➕ P(A o B) no es P(A) + P(B). Hay que restar lo que se cuenta dos veces: aquí 0,4897 contra 0,4327 de verdad.
- 🔍 La condicional es filtrar antes de promediar. P(compra) es 0,5777 y P(compra|mayorista) es 0,7240.
- 🔀 P(A|B) no es P(B|A). El 72,40% de los mayoristas compra, pero solo el 31,33% de las compras son de mayoristas.
- ⚖️ Bayes es la fórmula para darle la vuelta, y la pieza que manda es la tasa base.
- 🚨 Un detector con 99% de acierto sobre algo que pasa el 0,70% de las veces acierta el 41,11% de sus alarmas.
- 👻 Un filtro mal escrito no da error: devuelve cero filas y un
nan. Cuenta las filas después de filtrar.
Qué viene ahora
En el capítulo 8 vemos las tres distribuciones que de verdad se usan (binomial, Poisson y normal) y el teorema que explica por qué la campana aparece por todas partes aunque tus datos no sean campana 🔔