Capítulo 3 de 23 13 secciones 22 min

Compartir

Cuál de los tres promedios reportar

El ticket promedio es 803 soles y el 65,93% de las ventas está por debajo. Quién tiene razón, y por qué.

La media se usa cuando los datos son simétricos, la mediana cuando hay cola larga o valores extremos porque no se mueve con ellos, y la moda solo para categorías. En las ventas de este libro la media es 803,76 soles y la mediana 533,63, y el 65,93% de las ventas está por debajo de la media. Yo reporto las dos siempre. Y para tasas de crecimiento no sirve ninguna de las tres: ahí va la media geométrica, o proyectas crecimiento en un negocio que cayó.

Frase que se oye en todas las empresas: "nuestro ticket promedio es de 803 soles".

Vamos a ver si es verdad, y sobre todo si sirve para algo 🧐

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('media   %.2f' % v['monto'].mean())
print('mediana %.2f' % v['monto'].median())
print('moda    %.2f' % v['monto'].mode()[0])
media   803.76
mediana 533.63
moda    204.47

Tres medidas del mismo centro y tres respuestas distintas: 803, 533 y 204. Y cada una tiene su significado 👇

  • La media es lo que sale de sumar todo y repartirlo en partes iguales. Contesta "si todas las ventas hubieran sido iguales, ¿de cuánto serían?".
  • 🎯 La mediana es el valor de en medio cuando las ordenas. La mitad de las ventas está por debajo y la mitad por encima.
  • 🔁 La moda es el valor que más se repite.

Y ahora la pregunta del millón: ¿cuál llevo a la reunión?

x¯=1ni=1nxi

sumas todos los datos y los repartes en partes iguales entre cuántos hay

Antes de seguir, una pregunta: ¿cuánto compra un cliente típico de tu negocio? Si tienes el número en la cabeza, apúntalo. Al final del capítulo vas a saber si es la media o la mediana, y si esas dos se parecen 💰

Histograma de las 3.000 ventas con dos líneas verticales encima: la media en 803,76 soles y la mediana en 533,63. La mediana cae dentro del montón de ventas y la media queda a su derecha, arrastrada por la cola.
La media está a la derecha del montón porque unas pocas ventas enormes tiran de ella. La mediana se queda donde está la gente. Por eso el 65,93% de las ventas queda por debajo del "promedio".
Árbol de decisión para elegir medida de tendencia central: con valores extremos, mediana; con categorías, moda; en el resto, media, y siempre acompañada de cuánto varían los datos.
La regla práctica en una línea: si hay valores extremos, la media miente y la mediana no. Por eso los informes serios de ingresos usan mediana y los titulares usan media.

El dato que decide

debajo = (v['monto'] < v['monto'].mean()).mean()
print('ventas por debajo de la media: %.2f%%' % (100 * debajo))
print('asimetria: %.4f' % v['monto'].skew())
ventas por debajo de la media: 65.93%
asimetria: 1.3405

Ahí está 💥

El 65,93% de las ventas está por debajo del "promedio".

O sea que si sales de la reunión pensando "una venta típica es de 803 soles", sales pensando algo que le pasa a una minoría. Dos de cada tres ventas son más chicas que eso.

Esa asimetría de 1,3405 es el número que lo explica. Cuando es positiva quiere decir que hay cola por la derecha: muchas ventas chicas y unas pocas enormes que empujan la media hacia arriba. La mediana ni se entera, porque a ella solo le importa quién está en el medio de la fila, no cuánto pesa el último 🧘

Un solo dato, y mira lo que pasa

Esto es lo que más me gusta enseñar de este capítulo, porque se ve en dos segundos. Voy a añadir una sola venta de medio millón:

con_una_mas = pd.concat([v['monto'], pd.Series([500000.0])])

print('media   antes %.2f -> despues %.2f' % (v['monto'].mean(), con_una_mas.mean()))
print('mediana antes %.2f -> despues %.2f' % (v['monto'].median(), con_una_mas.median()))
media   antes 803.76 -> despues 970.10
mediana antes 533.63 -> despues 534.02

Una fila entre tres mil, y la media sube 166 soles. La mediana se mueve 39 céntimos.

Eso es lo que quiere decir que la mediana sea robusta: para moverla hay que mover a la gente de en medio, y un dato extremo no lo consigue por muy extremo que sea 💪

La media recortada, que es el término medio

Hay una opción intermedia que se usa poco y está muy bien: quitar un porcentaje de cada punta y promediar el resto.

from scipy import stats

for recorte in [0, 0.05, 0.10, 0.25]:
    print('recortando %2.0f%% de cada lado: %.2f'
          % (100 * recorte, stats.trim_mean(v['monto'], recorte)))
recortando  0% de cada lado: 803.76
recortando  5% de cada lado: 735.70
recortando 10% de cada lado: 679.65
recortando 25% de cada lado: 570.44

Mira la escalera: cuanto más recortas, más se acerca a la mediana. Con el 25% de cada lado sale 570,44, ya casi la mediana de 533,63.

Y tiene sentido: si recortaras el 50% de cada lado no quedaría nadie salvo el de en medio, o sea la mediana. De hecho probemos a pasarnos:

stats.trim_mean(v['monto'], 0.6)
ValueError: Proportion too big.

Ese error es correcto y hasta bonito 😄 No puedes recortar el 60% de cada lado porque no queda nada que promediar.

La media recortada es la que usan las competiciones deportivas cuando quitan la nota más alta y la más baja de los jueces. Sirve cuando sospechas de errores de captura en los extremos, pero quieres seguir usando toda la información del medio.

Barras de media y mediana para los cuatro segmentos y para el total. En Bodega, Horeca, Mayorista y Minimarket las dos barras tienen casi la misma altura; en la columna del total la media supera claramente a la mediana.
Dentro de cada segmento la media y la mediana coinciden, o sea que ahí no hay cola. La separación de 270 soles solo aparece al juntarlos: la fabricaba la mezcla de cuatro negocios, no las ventas.

Y ahora el hallazgo que no me esperaba

Hasta aquí todo iba según el manual:

  • Media 803
  • Mediana 533
  • Cola larga
  • Usa la mediana

Pero se me ocurrió mirar lo mismo dentro de cada segmento y me llevé una sorpresa 👀

print(v.groupby('segmento')['monto'].agg(['mean', 'median', 'count']).round(2))
               mean   median  count
segmento
Bodega       178.70   183.26    707
Horeca       755.18   743.00    742
Mayorista   1856.34  1869.26    750
Minimarket   414.92   413.74    801

Léelo despacio porque es importante: dentro de cada segmento, la media y la mediana son casi iguales.

Bodega: 178,70 y 183,26. Minimarket: 414,92 y 413,74. Mayorista: 1.856,34 y 1.869,26. En ninguno hay cola larga.

Y sin embargo en el total se separan 270 soles 🤯

Entonces, ¿de dónde salía la asimetría? No de las ventas: de la mezcla. Hay cuatro poblaciones muy distintas metidas en el mismo archivo, una que vende a 178 soles y otra que vende a 1.856. Al juntarlas, el montón se ve deforme aunque cada trozo sea simétrico.

print(v.groupby('segmento')['monto']
      .apply(lambda s: (s < s.mean()).mean()).round(4))
segmento
Bodega        0.4611
Horeca        0.5189
Mayorista     0.4853
Minimarket    0.5031
Name: monto, dtype: float64

Confirmado: dentro de cada segmento, alrededor del 50% de las ventas está por debajo de su media, que es lo que pasa cuando no hay cola. El 65,93% del total era un espejismo de la mezcla.

Esto cambia la recomendación práctica y me parece lo más útil del capítulo: antes de discutir si media o mediana, pregúntate si estás promediando cosas que no se parecen. Aquí el "ticket promedio de la empresa" no existe: existen cuatro tickets promedio y son cuatro negocios distintos 🏪

Esta idea, la de que un número global puede contar una historia que ningún grupo confirma, tiene nombre propio y su versión más salvaje sale en el capítulo 17: la paradoja de Simpson.

La media de las medias

Otra trampa clásica, y esta la he visto en informes que iban a directorio. Cuando ya tienes una media por grupo, es tentador promediar esas medias:

por_segmento = v.groupby('segmento')['monto'].agg(['mean', 'count'])

print('media de las medias: %.2f' % por_segmento['mean'].mean())
print('media de verdad:     %.2f' % v['monto'].mean())
media de las medias: 801.28
media de verdad:     803.76

Aquí la diferencia es chica, 2,48 soles, y te voy a decir exactamente por qué: porque los cuatro segmentos tienen casi el mismo número de filas (707, 742, 750, 801).

La media de las medias trata a los cuatro grupos como si pesaran lo mismo. Si Bodega tuviera 50 ventas y Mayorista 2.500, ese cálculo le daría a las 50 el mismo peso que a las 2.500 y el resultado sería un disparate.

Lo correcto cuando los grupos son de tamaños distintos es la media ponderada, que le da a cada grupo el peso que le toca:

x¯p=gngx¯ggng

cada grupo aporta a la media según cuántas filas tiene, y no uno por grupo

ponderada = np.average(por_segmento['mean'], weights=por_segmento['count'])
print('media ponderada: %.4f' % ponderada)
print('media de verdad: %.4f' % v['monto'].mean())
media ponderada: 803.7613
media de verdad: 803.7613

Idénticas hasta el último decimal, como tenía que ser ✅

El error más caro del capítulo

Y llegamos al que de verdad hace daño, porque este sí cambia decisiones de plata 💸

En el capítulo 1 calculamos cuánto cambia la venta mensual, y salió que la media de los cambios era +0,31%. Suena a que el negocio crece despacito. Vamos a comprobarlo:

mes = v.groupby(v['fecha'].dt.to_period('M'))['monto'].sum()
r = mes.pct_change().dropna()

print('media de los cambios mensuales: %+.4f%%' % (100 * r.mean()))
print('cambio real del primer al ultimo mes: %+.4f%%'
      % (100 * (mes.iloc[-1] / mes.iloc[0] - 1)))
media de los cambios mensuales: +0.3079%
cambio real del primer al ultimo mes: -19.6635%

La media dice +0,31% al mes. La realidad es que el negocio cayó 19,66% 😱

Y no hay ningún error de cálculo. La media aritmética de los porcentajes está bien calculada. Lo que pasa es que los porcentajes no se promedian sumando.

Piénsalo con un ejemplo de cabeza: si algo sube 50% y luego baja 50%, la media aritmética dice 0%. Pero 100 sube a 150 y luego baja a 75. Perdiste el 25%. Las subidas y las bajadas no son simétricas porque se aplican sobre bases distintas.

Lo que hay que usar es la media geométrica: multiplicar en vez de sumar, y sacar la raíz.

g=(i=1n(1+ri))1/n1

multiplicas todos los factores de crecimiento y sacas la raíz enésima, en vez de sumarlos y dividir

geometrica = np.prod(1 + r) ** (1 / len(r)) - 1

print('media geometrica: %+.4f%%' % (100 * geometrica))
print()
print('primer mes:                       %.2f' % mes.iloc[0])
print('proyectando con la aritmetica:    %.2f' % (mes.iloc[0] * (1 + r.mean()) ** len(r)))
print('proyectando con la geometrica:    %.2f' % (mes.iloc[0] * (1 + geometrica) ** len(r)))
print('lo que pasó de verdad:            %.2f' % mes.iloc[-1])
media geometrica: -1.2797%

primer mes:                       128800.46
proyectando con la aritmetica:    135711.18
proyectando con la geometrica:    103473.73
lo que pasó de verdad:            103473.73

La media geométrica reproduce el resultado real al céntimo. La aritmética se equivoca en 32.237 soles 📉

La regla, corta y para toda la vida: si lo que promedias son tasas, porcentajes o factores de crecimiento, la media es geométrica. Si son cantidades sueltas (soles, unidades, minutos), es aritmética.

Distribución con cola a la derecha con las tres medidas marcadas en sitios distintos: la moda en el pico, la mediana un poco a su derecha y la media más a la derecha todavía, arrastrada por la cola.
En una distribución simétrica las tres caen en el mismo punto y da igual cuál uses. Se separan justo cuando hay cola, que es el caso de casi todo lo que tiene que ver con dinero, y el orden es siempre este: la media es la que más se va detrás de los pocos valores altos.

Cuál reportar, en una tabla

SituaciónQué usarPor qué
Datos simétricos, sin extremosMediaUsa toda la información
Cola larga o valores extremosMedianaNo se mueve con un dato raro
Sospecha de errores en las puntasMedia recortadaTira las puntas y conserva el medio
Grupos de tamaños distintosMedia ponderadaCada grupo pesa lo que le toca
Tasas y crecimientosMedia geométricaLos porcentajes se multiplican
CategoríasModaEs la única que existe

Y una recomendación que vale más que la tabla: reporta las dos, media y mediana, siempre. Cuando se parecen, no pasa nada y quedas bien. Cuando se separan, esa separación es en sí misma el hallazgo 🌟

Y dos más que salen al promediar

np.average(v['monto'], weights=[1, 2, 3])
TypeError: Axis must be specified when shapes of a and weights differ.

La media ponderada necesita un peso por cada valor, y aquí hay 3.000 montos y tres pesos. Es el error de quien copia el ejemplo de la documentación sin cambiar la lista.

v.groupby('segmento')['monto'].agg('mediana')
AttributeError: 'SeriesGroupBy' object has no attribute 'mediana'

Se llama median. Te lo pongo porque a mí me pasa cada dos por tres: uno escribe en español sin darse cuenta, y el mensaje no dice "no sé qué es mediana", dice que el objeto no tiene ese atributo, que es más confuso de lo que debería 🙃

Practica 💪

1. El ticket promedio de verdad

Calcula el ticket promedio como lo querría oír el negocio: solo sobre las ventas que se cerraron. Da media y mediana.

cerradas = v.loc[v['compro'] == 1, 'monto_final_facturado']

print('ventas cerradas: %d' % len(cerradas))
print('media   %.2f' % cerradas.mean())
print('mediana %.2f' % cerradas.median())
ventas cerradas: 1733
media   833.10
mediana 570.61

833,10 de media y 570,61 de mediana, sobre 1.733 ventas cerradas.

Y la separación entre las dos sigue ahí, que ya sabemos por qué: la mezcla de segmentos. Si tuvieras que poner un solo número en un informe, yo pondría la mediana con la media al lado: "la venta típica cerrada es de 571 soles; el promedio es 833 porque unos pocos mayoristas compran mucho".

Esa frase de una línea es exactamente lo que hace que alguien te crea 🙌

2. La moda sobre una variable continua

Sacamos la moda de monto al principio y salió 204,47. ¿Cuántas veces aparece ese valor? ¿Sirve de algo?

moda = v['monto'].mode()[0]
print('la moda es %.2f y aparece %d veces de %d'
      % (moda, (v['monto'] == moda).sum(), len(v)))
la moda es 204.47 y aparece 3 veces de 3000

Tres veces. De tres mil 😅

La moda de una variable continua no significa nada. Con decimales, casi ningún valor se repite, así que gana el que por casualidad salió tres veces en vez de dos.

La moda sirve para categorías, donde sí hay repetición de verdad:

print('segmento mas frecuente:', v['segmento'].mode()[0],
      'con', (v['segmento'] == v['segmento'].mode()[0]).sum(), 'ventas')
segmento mas frecuente: Minimarket con 801 ventas

Eso sí es información. Y fíjate que aquí la moda es la única de las tres medidas que existe: no hay media ni mediana de "Minimarket".

Si de verdad quieres una moda de algo continuo, primero lo agrupas en tramos. Eso es un histograma y es todo el capítulo 5 📊

3. La media ponderada que devuelve nada

Calcula el descuento medio ponderado por unidades, o sea dándole más peso a las ventas grandes. Mira bien el resultado.

print(np.average(v['descuento'], weights=v['unidades']))
nan

nan. Sin error, sin aviso, sin nada 🫥

Y esto es un cambio de comportamiento que hay que tener grabado: mean() de pandas ignora los nulos, pero np.average de numpy los propaga. Un solo nulo entre tres mil, y todo el resultado es nulo.

Sinceramente prefiero el de numpy. Que te devuelva nan es feo pero es honesto: te obliga a decidir qué haces con los huecos. El de pandas decide por ti y no te lo cuenta.

Se arregla quitando las filas sin descuento, que es asumir que el vacío es "no sé" y no "cero":

con_dato = v.dropna(subset=['descuento'])

print('ponderada por unidades: %.6f'
      % np.average(con_dato['descuento'], weights=con_dato['unidades']))
print('simple:                 %.6f' % con_dato['descuento'].mean())
ponderada por unidades: 0.124926
simple:                 0.125202

12,49% contra 12,52%. Prácticamente igual, y eso también es un resultado: las ventas grandes no reciben más descuento que las chicas.

Si el descuento fuera por volumen, la ponderada saldría claramente más alta. No sale. O sea que aquí los descuentos se dan por otra cosa 🤔

4. ¿Y si los grupos fueran desiguales?

Vimos que la media de las medias fallaba por poco porque los segmentos son parecidos de tamaño. Fabrica el caso desigual: quédate con todas las ventas de Bodega y solo 50 de Mayorista, y compara otra vez.

truncado = pd.concat([
    v[v['segmento'] == 'Bodega'],
    v[v['segmento'] == 'Mayorista'].head(50),
])

medias = truncado.groupby('segmento')['monto'].agg(['mean', 'count'])
print(medias.round(2))
print()
print('media de las medias: %.2f' % medias['mean'].mean())
print('media de verdad:     %.2f' % truncado['monto'].mean())
              mean  count
segmento                 
Bodega      178.70    707
Mayorista  1988.51     50

media de las medias: 1083.61
media de verdad:     298.24

1.083,61 contra 298,24. Se equivoca en más del triple 😵

Y se ve por qué de un vistazo: la media de las medias le da a los 50 mayoristas el mismo peso que a las 707 bodegas. Cada mayorista pesa catorce veces más que una bodega.

Esto no es un caso de laboratorio, pasa todo el tiempo: promedias la satisfacción de tus cinco sucursales y una atiende a diez clientes y otra a mil. Si alguien te pasa medias ya calculadas, la primera pregunta es "¿sobre cuántos casos?". Sin el tamaño, una media no se puede combinar con otra 🚫

5. Media geométrica en algo que sí crece

El negocio del archivo cae. Fabrica una serie que crezca de verdad (100 que sube 20%, luego 30% y luego baja 40%) y comprueba con las dos medias cuál reconstruye el final.

tasas = np.array([0.20, 0.30, -0.40])
inicio = 100.0

final_real = inicio * np.prod(1 + tasas)
aritmetica = tasas.mean()
geometrica = np.prod(1 + tasas) ** (1 / len(tasas)) - 1

print('final real:                %.2f' % final_real)
print('con la aritmetica (%+.2f%%): %.2f'
      % (100 * aritmetica, inicio * (1 + aritmetica) ** len(tasas)))
print('con la geometrica (%+.2f%%): %.2f'
      % (100 * geometrica, inicio * (1 + geometrica) ** len(tasas)))
final real:                93.60
con la aritmetica (+3.33%): 110.34
con la geometrica (-2.18%): 93.60

La aritmética dice que creciste 3,33% al mes y que acabaste en 110. La realidad es que acabaste en 93,60, o sea perdiendo 🙃

Dos subidas fuertes y una bajada, y la media aritmética se queda con la sensación de las subidas. La geométrica clava el resultado, otra vez.

Fíjate en el detalle: los signos ni siquiera coinciden. Una dice +3,33% y la otra -2,18%. No es que una sea más precisa: es que la aritmética responde a otra pregunta, la de "cuánto sube en promedio cada mes por separado", que no es la que nadie quiere saber.

6. La mediana también se puede romper

Llevamos medio capítulo diciendo que la mediana es robusta. Encuentra el caso en que no sirve: calcula media y mediana de compro y de satisfaccion.

print('compro       -> media %.4f | mediana %.1f'
      % (v['compro'].mean(), v['compro'].median()))
print('satisfaccion -> media %.4f | mediana %.1f'
      % (v['satisfaccion'].mean(), v['satisfaccion'].median()))
compro       -> media 0.5777 | mediana 1.0
satisfaccion -> media 3.0098 | mediana 3.0

La mediana de compro es 1,0. O sea: "la venta típica compró" 🤨

Es verdad y es inútil. Ordena las 3.000 filas: primero los 1.267 ceros y luego los 1.733 unos. La de en medio, la 1.500, cae dentro del bloque de unos, y la mediana devuelve 1. Habría devuelto 1 igual si el reparto fuera 49% contra 51%, y también con 1% contra 99%.

La mediana de una variable binaria solo tiene dos respuestas posibles, así que no puede decirte cuánto. La buena aquí es la media, 0,5777, porque en una columna 0/1 la media es la proporción, como vimos en el capítulo 2.

Moraleja: no hay una medida que gane siempre. Robusta no quiere decir correcta, quiere decir que no se mueve con los extremos. Para elegir bien hay que saber qué representa la columna, que es justo lo del capítulo 2 😌

7. Tu resumen de dos líneas

Escribe una función que reciba una columna y devuelva la frase que pondrías en un informe.

def resume(serie, nombre):
    media, mediana = serie.mean(), serie.median()
    brecha = 100 * (media - mediana) / mediana
    if abs(brecha) < 10:
        return ('%s: %.2f de promedio, y la mediana casi igual (%.2f). '
                'Reporta la media.' % (nombre, media, mediana))
    return ('%s: la típica es %.2f, pero el promedio sube a %.2f (%+.0f%%) '
            'por la cola de arriba. Reporta la mediana.'
            % (nombre, mediana, media, brecha))


print(resume(v['monto'], 'monto'))
print()
print(resume(v.loc[v['segmento'] == 'Bodega', 'monto'], 'monto en Bodega'))
monto: la típica es 533.63, pero el promedio sube a 803.76 (+51%) por la cola de arriba. Reporta la mediana.

monto en Bodega: 178.70 de promedio, y la mediana casi igual (183.26). Reporta la media.

La misma columna, dos veredictos distintos, y los dos correctos 🎯

Ese 10% del umbral es mío y lo puedes mover. Lo importante no es el número sino que la decisión esté escrita en algún sitio y no dependa de cómo te levantaste ese día.

8. ¿En qué columnas se separan?

Calcula media y mediana de las cuatro numéricas y mira en cuáles se alejan.

for col in ['monto', 'unidades', 'descuento', 'satisfaccion']:
    s = v[col].dropna()
    print('%-13s media %8.2f   mediana %8.2f   se llevan %6.2f'
          % (col, s.mean(), s.median(), s.mean() - s.median()))
monto         media   803.76   mediana   533.63   se llevan 270.13
unidades      media    11.60   mediana    12.00   se llevan  -0.40
descuento     media     0.13   mediana     0.13   se llevan  -0.00
satisfaccion  media     3.01   mediana     3.00   se llevan   0.01

Tres de cuatro dan lo mismo, y una se lleva 270 soles 😮

Ahí tienes el detector más barato que existe: cuando media y mediana se separan, hay cola. Y solo pasa con el monto, porque unos pocos pedidos enormes tiran del promedio hacia arriba.

Úsalo al revés también: si en tu tabla las dos se parecen, puedes reportar la media tranquila. La diferencia entre las dos es la que te dice cuál elegir, y no hace falta ningún gráfico 📏

9. La media de las medias no es la media

Calcula el promedio del monto por segmento y después promedia esos cuatro números. Compáralo con el promedio global.

por_seg = v.groupby('segmento')['monto'].agg(['mean', 'median', 'count'])
por_seg['pesa'] = por_seg['count'] / len(v)
print(por_seg.round(2).to_string())
print()
print('media global      : %.2f' % v['monto'].mean())
print('media de las medias: %.2f' % por_seg['mean'].mean())
               mean   median  count  pesa
segmento
Bodega       178.70   183.26    707  0.24
Horeca       755.18   743.00    742  0.25
Mayorista   1856.34  1869.26    750  0.25
Minimarket   414.92   413.74    801  0.27

media global      : 803.76
media de las medias: 801.28

Aquí se parecen, 803,76 contra 801,28, y eso no siempre pasa ⚠️

Se parecen porque los cuatro segmentos tienen casi el mismo número de pedidos: mira la columna pesa, todos rondan el 25%. Si un segmento tuviera el 70% de las filas, promediar los cuatro promedios daría un número muy distinto del real, porque estarías dándole a un grupo de 100 pedidos el mismo peso que a uno de 2.000.

Es de los errores más comunes que hay en un informe, y tiene nombre propio: promediar promedios sin pesarlos. La versión gorda de esto es la paradoja de Simpson del capítulo 17 🕳️

Trescientos soles entre dos formas de decir lo normal

Antes de cerrar el capítulo, la trampa. Las dos cuentas están bien hechas y una de las dos te va a hacer quedar mal 💸

La trampa

Te piden cuánto compra un cliente típico. Sacas el promedio, que es lo que significa típico, y lo mandas.

print(round(v['monto'].mean(), 2))
# 803.76

print(round(v['monto'].median(), 2))
# 533.63
Qué está mal

Doscientos setenta soles de diferencia entre dos formas de decir "lo normal" 💸

La venta más grande del archivo es de 4.236,71 y la asimetría es 1,3405, o sea que hay una cola larga de ventas grandes tirando de la media hacia arriba. La mediana no se entera de esa cola: parte el archivo por la mitad y ya.

Y las dos son correctas. El problema es cuál contesta la pregunta que te hicieron. Si te preguntan cuánto compra un cliente típico, la mediana. Si te preguntan cuánto vamos a facturar con mil clientes, la media, porque la media sí sabe de las ventas grandes.

La regla corta: si la media y la mediana se separan, se reporta la mediana y se dice que están separadas. Yo pongo las dos siempre, y esa sola costumbre me ha ahorrado varias conversaciones incómodas.

Comprueba que lo tienes

Dentro de cada segmento la media y la mediana coinciden, y en el total se separan 270 soles. ¿Qué está pasando?

  • Estamos promediando cuatro negocios que venden a niveles distintos
  • Hay ventas atípicas que estiran la media del total
  • La mediana del total está mal calculada
  • Falta limpiar los datos

Lo que te llevas

  • 📊 Media 803,76, mediana 533,63, y el 65,93% de las ventas por debajo del "promedio". Ese porcentaje es el que hay que mirar para saber si la media engaña.
  • 🪨 Una sola venta de medio millón mueve la media 166 soles y la mediana 39 céntimos. Eso es ser robusta.
  • 🧩 Dentro de cada segmento la media y la mediana coinciden. La asimetría del total no venía de las ventas, venía de mezclar cuatro negocios distintos.
  • ⚖️ La media de las medias solo funciona si los grupos son del mismo tamaño. Con 707 bodegas y 50 mayoristas se equivocó en más del triple.
  • 📉 Para tasas y crecimientos, media geométrica. La aritmética proyectaba +0,31% mensual en un negocio que cayó 19,66%.
  • 🕳️ np.average devuelve nan si hay un solo nulo. mean() de pandas los ignora sin decírtelo.
  • ✌️ Reporta siempre las dos. Si se parecen, bien; si se separan, esa separación es el hallazgo.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

Si la media y la mediana se separan, esa separación es el hallazgo.

Y para calcular estos tres sobre tus propios datos, con groupby y en tres líneas, está el libro de Python 🐍

Qué viene ahora

El centro es media historia. En el capítulo 4 vamos a la otra mitad: cuánto se separan los datos de ese centro. Y ahí sale por qué dos negocios con el mismo promedio pueden ser completamente distintos 📏

Preguntas frecuentes

¿Cuál es la diferencia entre media y mediana?

La media suma todo y divide; la mediana es el valor del medio cuando ordenas los datos. Un solo dato enorme mueve la media y no mueve la mediana, y por eso los sueldos y los tickets se reportan con mediana.

¿Qué es la media geométrica y cuándo se usa?

Se multiplican los valores y se saca la raíz enésima. Se usa cuando los números son tasas de crecimiento: promediar crecimientos con la media normal da un número que no existe.

¿Qué es la media ponderada?

Cada dato pesa distinto en el promedio. Es lo que hay que usar cuando una tienda vendió mil pedidos y otra vendió diez: si las promedias igual, la chica cuenta lo mismo que la grande.

¿Cómo se calcula la mediana?

Ordenas los datos y tomas el del medio. Si son pares, el promedio de los dos del medio.

¿Cuál reporto, la media o la mediana?

Mira el histograma. Si es simétrico dan casi lo mismo y da igual. Si tiene cola, la media se va detrás de la cola y hay que reportar la mediana, o las dos y decir por qué se separan.

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.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?