El capítulo anterior fue cómo se dice. Este es qué se dice, que es más difícil 🌸
La proporción, para que veas el tamaño del descarte
import pandas as pd import numpy as np URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv' df = pd.read_csv(URL) d = df[['canal', 'compro', 'segmento', 'ciudad']].dropna(subset=['canal', 'compro']) sub = d[d['canal'].isin(['WhatsApp', 'Web'])] print('celdas del archivo:', df.size) print('filas que uso :', len(sub)) print('numeros del titular:', 4)
celdas del archivo: 42518 filas que uso : 1526 numeros del titular: 4
42.518 celdas para decir cuatro números. Analizar es sobre todo descartar, y nadie te enseña con qué criterio.
El general, y lo que esconde
w = sub[sub['canal'] == 'WhatsApp']['compro'] b = sub[sub['canal'] == 'Web']['compro'] print('diferencia general:<p></p><ul class="grid grid-cols-1 md:grid-cols-3 gap-3 my-6 list-none pl-0"><li class="bg-white rounded-xl border border-gray-100 shadow-sm px-4 py-3 flex items-start gap-3"><span class="w-1.5 h-1.5 rounded-full bg-pink flex-shrink-0 mt-2"></span><span class="text-gray-600 font-poppins text-sm leading-relaxed">'</span></li><li class="bg-white rounded-xl border border-gray-100 shadow-sm px-4 py-3 flex items-start gap-3"><span class="w-1.5 h-1.5 rounded-full bg-pink flex-shrink-0 mt-2"></span><span class="text-gray-600 font-poppins text-sm leading-relaxed">Round(float(w.mean() - b.mean()), 4))</pre> <pre class="salida">diferencia general: 0.0694</pre> <p>6</span></li><li class="bg-white rounded-xl border border-gray-100 shadow-sm px-4 py-3 flex items-start gap-3"><span class="w-1.5 h-1.5 rounded-full bg-pink flex-shrink-0 mt-2"></span><span class="text-gray-600 font-poppins text-sm leading-relaxed">9 puntos</span></li></ul><pre>Ese es el titular del capítulo 19. Ahora ábrelo:<p></p> <pre>for seg in sorted(sub['segmento'].dropna().unique()): s = sub[sub['segmento'] == seg] a = s[s['canal'] == 'WhatsApp']['compro'] c = s[s['canal'] == 'Web']['compro'] print(seg.ljust(11), 'WhatsApp', round(float(a.mean()), 3), '| Web', round(float(c.mean()), 3), '| dif', round(float(a.mean() - c.mean()), 3))
Bodega WhatsApp 0.452 | Web 0.389 | dif 0.063 Horeca WhatsApp 0.651 | Web 0.635 | dif 0.016 Mayorista WhatsApp 0.815 | Web 0.74 | dif 0.075 Minimarket WhatsApp 0.694 | Web 0.57 | dif 0.123
Mira la distancia: Minimarket sube 12,3 puntos y Horeca 1,6. Casi ocho veces.
El 6,9 general no está mal calculado, es un promedio correcto. Lo que pasa es que cambia la recomendación: con el general, mueves a todos los clientes a WhatsApp. Con la apertura, no mueves a Horeca, porque el esfuerzo no se va a pagar ahí.
Ese es el criterio entero de este capítulo: se enseña lo que cambia la decisión. No lo que es interesante, no lo que costó trabajo, no lo que quedó bonito.
Y entonces por qué no enseñar todos los cortes
La tentación obvia: si abrir por segmento sirvió, abro por todo.
segs = sub['segmento'].nunique() ciu = sub['ciudad'].nunique() print('segmentos:', segs, '| ciudades:', ciu) print('comparaciones posibles:', segs + ciu + segs * ciu)
segmentos: 4 | ciudades: 9 comparaciones posibles: 49
49 comparaciones con dos columnas. Con cinco columnas son cientos.
Y con 49 comparaciones, alguna va a salir significativa por casualidad. Es el p-hacking del capítulo 17, y no deja de serlo porque tú no lo estuvieras buscando a propósito.
La diferencia entre abrir por segmento y hacer p-hacking no está en el código: está en cuándo lo decidiste. Si el corte lo elegiste antes de mirar, porque el negocio funciona distinto en cada segmento, es análisis. Si lo elegiste después, porque ese era el que daba, es otra cosa.
Por eso la lista de cortes que vas a mirar se escribe antes, igual que el criterio de la prueba A/B del capítulo 18.
La tabla que nadie lee
t = sub.pivot_table(index='segmento', columns='canal', values='compro', aggfunc='mean').round(3) print(t.to_string()) print('celdas:', t.size)
canal Web WhatsApp segmento Bodega 0.389 0.452 Horeca 0.635 0.651 Mayorista 0.740 0.815 Minimarket 0.570 0.694 celdas: 8
Ocho celdas, que ya es una tabla chiquita, y aun así nadie la va a leer en una reunión: van a mirar los números buscando el suyo.
Lo que sí se lee es la frase que sale de ahí:
"Minimarket es donde más sube, doce puntos. Horeca casi no se mueve, así que ahí no lo movería."
La tabla va igual, en la lámina o en el anexo, porque quien quiera verificar tiene derecho. Pero la tabla acompaña a la frase, no la sustituye 🧾
Lo que no te puedes callar
Hasta acá hablamos de elegir. Hay una categoría aparte: lo que se dice aunque estropee tu titular.
- La limitación que cambiaría la conclusión. Si las filas no son independientes, si el periodo fue raro, si un segmento tiene 30 filas.
- El corte que te salió en contra. Si abriste por ciudad y en dos no funciona, eso se dice. Callarlo es lo que hace que la próxima vez no te crean.
- Que es correlación y no causa, cuando no hubo asignación al azar.
- De dónde salieron los supuestos del cálculo de dinero.
La regla que uso: si alguien lo descubriera después de decidir, ¿se sentiría engañado? Si la respuesta es sí, va dentro 💛
El error de cortar hasta que no quede nada
sub[sub['ciudad'] == 'Tarapoto']['compro'].value_counts(normalize=True).loc[1]
KeyError: 1
Un KeyError pelado, que no dice nada de lo que pasó de verdad:
en Tarapoto no hay ninguna venta, así que no hay nada que contar y no existe la
clave.
Este error sale cuando encadenas filtros buscando el corte que dé bonito. Y
la versión peligrosa es la que no revienta: te quedan cuatro
filas, sale un 75% espectacular, y ese número acaba en una lámina sin el
n = 4 al lado.
La costumbre que lo evita: imprimir el tamaño del grupo junto al porcentaje, siempre. No hay excepciones para esa regla.
Lo que te llevas
- De 42.518 celdas salen cuatro números. Analizar es descartar.
- Se enseña lo que cambia la decisión.
- El promedio general puede ser correcto y esconder lo que importa.
- Elegir el corte después de ver los resultados es p-hacking.
- La tabla acompaña la frase, no la sustituye.
- Lo que se calla se elige; la limitación que cambiaría la conclusión no se elige, va dentro.
- El porcentaje sin el tamaño del grupo al lado es medio dato.
Comprueba que lo tienes
El promedio general dice +6,9 puntos, pero por segmento va de +1,6 en Horeca a +12,3 en Minimarket. ¿Qué llevas a la reunión?
- El general y la apertura por segmento, porque cambian la recomendación: Horeca no se mueve
- Solo el general, porque es el resultado del análisis
- Solo la apertura, porque el detalle siempre es mejor
- Las 49 comparaciones posibles, para que decidan ellos
Ejercicios
1. Siempre el n al lado del porcentaje
Mira cuánto cambia la lectura cuando aparece el tamaño.
for ciudad in sorted(sub['ciudad'].dropna().unique()): s = sub[sub['ciudad'] == ciudad]['compro'] print(ciudad.ljust(10), 'convierte', round(float(s.mean()), 3), '| n =', len(s))
Arequipa convierte 0.613 | n = 284 Chiclayo convierte 0.59 | n = 244 Cusco convierte 0.635 | n = 244 LIMA convierte 0.667 | n = 66 Lima convierte 0.545 | n = 55 Líma convierte 0.597 | n = 67 Piura convierte 0.624 | n = 263 Trujillo convierte 0.631 | n = 244 lima convierte 0.593 | n = 59
Dos cosas, y las dos importan.
La primera: hay cuatro maneras de escribir Lima y cada una tiene su fila. Juntas suman 247 ventas, más que cualquier otra ciudad, y separadas parecen cuatro sitios chiquitos. Ese es el trabajo de limpieza que va antes de cualquier informe.
La segunda: sin el n al lado, las nueve líneas parecen igual de
sólidas. Con el n, se ve que ese 66,7% de "LIMA" sale de 66
ventas.
2. El corte que te sale en contra
Búscalo a propósito, porque es el que hay que contar.
for seg in sorted(sub['segmento'].dropna().unique()): s = sub[sub['segmento'] == seg] a = s[s['canal'] == 'WhatsApp']['compro'] c = s[s['canal'] == 'Web']['compro'] dif = float(a.mean() - c.mean()) se = float(np.sqrt(a.var(ddof=1) / len(a) + c.var(ddof=1) / len(c))) cruza = (dif - 1.96 * se) < 0 < (dif + 1.96 * se) print(seg.ljust(11), 'dif', round(dif, 3), '| intervalo cruza el cero:', cruza)
Bodega dif 0.063 | intervalo cruza el cero: True Horeca dif 0.016 | intervalo cruza el cero: True Mayorista dif 0.075 | intervalo cruza el cero: True Minimarket dif 0.123 | intervalo cruza el cero: False
Tres de los cuatro cruzan el cero. Solo Minimarket se sostiene.
O sea que de la apertura entera, lo único que puedes afirmar es "en Minimarket sube y en los otros tres no lo sabemos". Bodega y Mayorista tienen una dirección que apunta bien y una muestra que no alcanza para probarla, porque al abrir cada grupo se queda con unas 180 filas y el intervalo se ensancha, como viste en el capítulo 19.
Fíjate en lo cómodo que sería callarse esta línea y presentar los cuatro números como si los cuatro dijeran algo. Y fíjate en lo distinto que suena el informe honesto: no es peor, es más útil, porque le dice a quien decide dónde poner el esfuerzo primero 💛
3. Cuántas comparaciones hacen falta para encontrar algo
La cuenta que explica el p-hacking sin fórmulas.
for k in (1, 5, 20, 49): p = 1 - 0.95 ** k print('con', str(k).rjust(2), 'comparaciones, probabilidad de al menos un', 'falso positivo:', round(p * 100, 1), '%')
con 1 comparaciones, probabilidad de al menos un falso positivo: 5.0 % con 5 comparaciones, probabilidad de al menos un falso positivo: 22.6 % con 20 comparaciones, probabilidad de al menos un falso positivo: 64.2 % con 49 comparaciones, probabilidad de al menos un falso positivo: 91.9 %
Con las 49 comparaciones que tenía a mano, la probabilidad de que al menos una salga significativa siendo mentira es del 91,9%.
Por eso no es una cuestión de honestidad personal: aunque no estés haciendo trampa, si miras cuarenta y nueve cortes vas a encontrar uno. La defensa es decidir los cortes antes, y decir cuántos miraste.
4. El resumen de una línea, generado
Que la frase salga del dato y no de tu memoria.
filas = [] for seg in sorted(sub['segmento'].dropna().unique()): s = sub[sub['segmento'] == seg] a = s[s['canal'] == 'WhatsApp']['compro'] c = s[s['canal'] == 'Web']['compro'] filas.append((float(a.mean() - c.mean()), seg)) filas.sort(reverse=True) mejor, peor = filas[0], filas[-1] print('El mayor efecto esta en', mejor[1], 'con', round(mejor[0] * 100, 1), 'puntos.') print('El menor esta en', peor[1], 'con', round(peor[0] * 100, 1), 'puntos.')
El mayor efecto esta en Minimarket con 12.3 puntos. El menor esta en Horeca con 1.6 puntos.
Dos líneas que se pegan al informe tal cual. La gracia no es ahorrarse escribir: es que si mañana entran datos nuevos y Horeca pasa a ser el mejor, la frase cambia sola y tú te enteras.
5. La limitación, medida en vez de intuida
Cuenta cuántos grupos tienen menos de 100 filas.
chicos = [] for seg in sorted(sub['segmento'].dropna().unique()): for ciudad in sorted(sub['ciudad'].dropna().unique()): n = len(sub[(sub['segmento'] == seg) & (sub['ciudad'] == ciudad)]) if n < 100: chicos.append(n) print('cruces segmento por ciudad:', 4 * 9) print('con menos de 100 filas :', len(chicos)) print('el mas chico :', min(chicos))
cruces segmento por ciudad: 36 con menos de 100 filas : 36 el mas chico : 10
Los 36 cruces tienen menos de 100 filas y el más chico tiene 10. Esa frase, tal cual, es la limitación que va en el informe: "no abro por segmento y ciudad a la vez porque el cruce más chico tiene diez ventas".
Decirlo medido es muy distinto de decir "no había suficientes datos". Lo primero es un análisis; lo segundo suena a excusa.
6. La prueba del engaño
Aplica la regla a tres cosas que podrías callarte.
casos = [
('use una prueba t de Welch y no de Student', False),
('los cuatro intervalos por segmento cruzan el cero', True),
('tarde tres dias en limpiar las ciudades', False),
('los clientes se repiten entre filas', True),
]
for texto, va_dentro in casos:
print(('VA DENTRO ' if va_dentro else 'se puede omitir'), texto)
se puede omitir use una prueba t de Welch y no de Student VA DENTRO los cuatro intervalos por segmento cruzan el cero se puede omitir tarde tres dias en limpiar las ciudades VA DENTRO los clientes se repiten entre filas
Las dos que van dentro tienen algo en común: si alguien las descubriera después de decidir, se sentiría engañado. Las otras dos son trabajo tuyo, y el trabajo tuyo va en el anexo o no va.
Esa es la prueba entera, y se puede aplicar en diez segundos antes de mandar cualquier informe 💛
Practica este capítulo 📓
Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.
¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.