Capítulo 1 de 16 10 secciones 15 min

¿Es real o es ruido?

La pregunta que decide si una reunión de una hora tenía sentido, medida sobre las ventas de verdad.

La estadística sirve para responder una sola pregunta: esta diferencia que estoy viendo, ¿es real o es lo que varía normalmente? En las ventas de este libro, un mes cualquiera se mueve 14,41% respecto al anterior sin que haya pasado nada. O sea que presentar una subida del 3% como un logro es presentar ruido. La estadística descriptiva resume lo que tienes; la inferencial dice qué de eso aguanta.

Hola! Bienvenida al libro 🌸

Voy a empezar contándote una reunión que he visto muchísimas veces, en empresas grandes y chicas, y que siempre termina igual.

Alguien proyecta una lámina: "las ventas subieron 3% este mes". Y ahí arranca la hora. Que si fue la campaña, que si el nuevo vendedor, que si el clima. Se reparten felicitaciones, se decide repetir lo que sea que se hizo, y se agenda un seguimiento.

Nadie hace la única pregunta que había que hacer: ¿cuánto se mueven las ventas normalmente?

Porque si las ventas se mueven 14% de un mes a otro sin que pase nada, ese 3% no es un logro. Es el ruido de siempre disfrazado de noticia. Y la reunión entera, con las seis personas que estaban dentro, se acaba de ir a la basura 🗑️

Eso es la estadística. No es la fórmula de la desviación estándar ni la tabla de la campana. Es esta pregunta:

¿Esta diferencia que estoy viendo es real, o es lo que varía normalmente?

Todo lo demás del libro son herramientas para contestarla.

Vamos a medirlo con datos de verdad

No te voy a pedir que me creas. Vamos a medir cuánto se mueven unas ventas reales, y de ahí sale si el 3% era noticia o no.

Trabajamos con el mismo archivo de todos los libros: las ventas de una distribuidora peruana, con la suciedad puesta a propósito porque es la que te vas a encontrar 🧹

import pandas as pd

URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'

v = pd.read_csv(URL)
print(v.shape)
print(v[['fecha', 'monto']].head(3))
(3037, 14)
        fecha   monto
0  2025-06-05  480.37
1  2026-01-05  524.90
2  2026-04-29  154.83

3.037 ventas y 14 columnas. Se ve bien, ¿no? Pues mira lo que pasa cuando intento sacar el mes de cada venta.

El primer golpe

v['fecha'].dt.month
AttributeError: Can only use .dt accessor with datetimelike values

Tranqui, no rompiste nada 🙂 pandas te está diciendo algo importante: fecha no es una fecha. Es texto que parece fecha.

El accesorio .dt (de datetime) es el que da acceso a año, mes, día y día de la semana. Solo funciona sobre columnas que pandas sabe que son fechas, y aquí leyó el CSV y lo dejó todo como texto porque nadie le dijo lo contrario.

Lo mismo pasa con monto. Así que antes de calcular nada, se convierte:

v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))

iso = pd.to_datetime(v['fecha'], format='%Y-%m-%d', errors='coerce')
resto = iso.isna()
iso[resto] = pd.to_datetime(v.loc[resto, 'fecha'], format='%d/%m/%Y')
v['fecha'] = iso

print(v['fecha'].min().date(), v['fecha'].max().date())
2025-01-01 2026-06-24

Un año y medio de ventas, del 1 de enero de 2025 al 24 de junio de 2026.

Eso de convertir en dos pasos es porque el archivo trae las fechas escritas de dos formas distintas, y lo vemos con calma en el capítulo 2. Por ahora quédate con que ya son fechas de verdad.

Cuánto se mueven las ventas

Ahora sí. Sumo lo vendido por mes:

mes = v.groupby(v['fecha'].dt.to_period('M'))['monto'].sum()
print(mes.round(0).head(6))
print('meses:', len(mes))
fecha
2025-01    128800.0
2025-02    132632.0
2025-03    140994.0
2025-04    115595.0
2025-05    161049.0
2025-06    139212.0
Freq: M, Name: monto, dtype: float64
meses: 18

Y ahora la parte que importa: cuánto cambia cada mes respecto al anterior. pct_change() hace exactamente eso 📈

cambio = (mes.pct_change() * 100).dropna()
print(cambio.round(1).head(8))
fecha
2025-02     3.0
2025-03     6.3
2025-04   -18.0
2025-05    39.3
2025-06   -13.6
2025-07     3.3
2025-08   -14.7
2025-09    16.0
Freq: M, Name: monto, dtype: float64

Mírala bien un segundo, porque esta tabla ya contesta la pregunta de la reunión.

Febrero subió 3,0%. Ese es exactamente el número de la lámina. Y ahora mira los que vienen: abril bajó 18%, mayo subió 39%, agosto bajó 14,7%.

El 3% de febrero, al lado de eso, no se distingue de nada.

El número que había que llevar a la reunión

print('media del cambio:      %.2f%%' % cambio.mean())
print('media en valor absoluto: %.2f%%' % cambio.abs().mean())
print('el peor mes:           %.2f%%' % cambio.min())
print('el mejor mes:          %.2f%%' % cambio.max())
print('meses que se movieron menos de 3%%: %d de %d'
      % ((cambio.abs() < 3).sum(), len(cambio)))
media del cambio:      0.44%
media en valor absoluto: 14.41%
el peor mes:           -32.36%
el mejor mes:          39.32%
meses que se movieron menos de 3%: 3 de 17

Ahí está, y son cuatro líneas 👇

  • 📊 El mes promedio se mueve 14,41% respecto al anterior. En valor absoluto, o sea sin importar si sube o baja.
  • ⚖️ La media del cambio con signo es 0,44%. O sea que a lo largo del año y medio las subidas y las bajadas casi se cancelan: el negocio no está creciendo ni cayendo, está oscilando.
  • 🎢 El rango va de -32,36% a +39,32%.
  • 🎯 Solo 3 meses de 17 se movieron menos de 3%.

Un 3% en este negocio es de los meses más planos que existen. Es literalmente lo contrario de una noticia.

Fíjate en lo poco que hizo falta: agrupar por mes, sacar el cambio porcentual y mirar la media del valor absoluto. Eso ya salva una hora de reunión, y no hemos usado ni una fórmula todavía 💪

El atajo que se inventa medio año

Ahora te voy a enseñar el error, porque este me pareció grave de verdad cuando lo vi.

Convertir las fechas en dos pasos es feo. Y pandas ofrece un atajo precioso que se ve muchísimo por ahí: format='mixed' le dice "descúbrelo tú", y dayfirst=True le dice "en mi país el día va primero".

Parece exactamente lo que necesitamos. Míralo:

atajo = pd.to_datetime(pd.read_csv(URL)['fecha'], format='mixed', dayfirst=True)
print('meses con el atajo:', atajo.dt.to_period('M').nunique())
print('meses de verdad:   ', v['fecha'].dt.to_period('M').nunique())
print('fechas cambiadas:  ', (atajo != v['fecha']).sum())
meses con el atajo: 24
meses de verdad:    18
fechas cambiadas:   847

Léelo otra vez 😳

El atajo no dio error. No avisó. No puso un warning. Se llevó por delante 847 fechas y se inventó seis meses que no existen.

Lo que pasó es esto: dayfirst=True se aplica también a las fechas que ya venían bien escritas. Cuando le llega 2025-06-05, que es el 5 de junio, ve que el día cabe en el hueco del mes y lo cambia de sitio: te devuelve el 6 de mayo. Solo se salvan las fechas donde el día es 13 o más, porque ahí no hay confusión posible.

Y ahora piensa qué pasa con todo lo que hicimos antes. El análisis mensual, la estacionalidad, el "cuánto se mueve el negocio": todo calculado sobre meses inventados, sin un solo mensaje rojo en pantalla.

Los errores que no dan error son los que hacen daño. Los que te tiran el programa los arreglas y ya. Estos llegan a una diapositiva 📉

La forma de cazarlo es la de siempre: después de convertir fechas, mira el mínimo, el máximo y cuántos meses distintos hay. Si tu archivo cubre año y medio y salen 24 meses, algo se rompió.

Las dos mitades de la estadística

Con eso ya se entiende la división del libro, que es sencilla:

MitadQué contestaEjemplo de este capítulo
Descriptiva Qué hay en los datos que tengo El mes promedio se mueve 14,41%
Inferencial Qué puedo afirmar de lo que NO medí ¿Ese 3% aguanta, o cae dentro del ruido?

La descriptiva es honesta y aburrida: describe lo que tienes y no promete nada más. La inferencial es la que da miedo y la que se usa mal, porque es la que se atreve a decir "esto también pasa fuera de mi muestra".

Los capítulos 2 al 6 son descriptiva. Del 7 al 13, inferencia. Y el 14 son los errores que invalidan un análisis entero, que casi todos viven en la segunda mitad.

Señal y ruido, viéndolos de cerca

Antes de cerrar quiero que veas los dos con los ojos, porque una vez que los distingues ya no se te olvida.

La columna compro vale 1 si esa venta se cerró. Voy a mirar el porcentaje de cierre por ciudad:

print((v.groupby('ciudad')['compro'].mean() * 100).round(1))
ciudad
Arequipa    57.6
Chiclayo    57.0
Cusco       57.3
LIMA        60.0
Lima        61.0
Líma        55.1
Piura       57.1
Trujillo    59.4
lima        56.1
Name: compro, dtype: float64

Dos cosas 👀

La primera, que te habrá saltado a la cara: hay nueve ciudades donde debería haber seis. Lima está escrita de cuatro formas distintas y para pandas son cuatro ciudades diferentes. Eso lo limpiamos en el capítulo 2, y es el motivo por el que la limpieza va antes que la estadística: no hay medida que sobreviva a datos sucios.

La segunda, la que nos importa hoy: los números se parecen todos. Del 55,1% al 61,0%, y con las cuatro Limas mezcladas ahí dentro. Nada destaca.

Ahora mira lo mismo por segmento de cliente:

print((v.groupby('segmento')['compro'].mean() * 100).round(1))
segmento
Bodega        37.6
Horeca        63.2
Mayorista     72.3
Minimarket    56.9
Name: compro, dtype: float64

Esto ya es otra cosa 🔥

Una bodega cierra el 37,6% y un mayorista el 72,3%. Casi el doble. Eso no se parece en nada a lo de ciudad.

Eso es señal. Lo de ciudad es ruido. Y de momento lo estamos diciendo a ojo, que es exactamente lo que quiero que dejes de hacer: en el capítulo 11 le ponemos un número a cada uno y sale que la diferencia por ciudad tiene un valor p de 0,9813, o sea "no hay nada aquí", mientras que la de segmento sale con una probabilidad tan chica que hay que escribirla en notación científica.

Pero fíjate en lo importante: a ojo también se veía. Los métodos del libro no están para descubrir lo que salta a la vista, sino para los casos dudosos, que son casi todos.

Practica 💪

Los ejercicios son la mitad del libro. Intenta antes de abrir.

Comprueba que lo tienes

Las ventas de este mes subieron 3% y el mes promedio se mueve 14,41%. ¿Qué reportas?

  • Que el mes entró dentro de lo normal, y digo cuánto es lo normal
  • Que subieron 3%, que es un dato cierto
  • Que hay una tendencia al alza que hay que sostener
  • Que hay que investigar qué lo causó
1. Cuánto se mueven las unidades

Hicimos el ejercicio con el monto. Hazlo con unidades: suma por mes, cambio porcentual y media en valor absoluto. ¿Se mueve más o menos que el dinero?

u = v.groupby(v['fecha'].dt.to_period('M'))['unidades'].sum()
cambio_u = (u.pct_change() * 100).dropna()
print('unidades, media absoluta: %.2f%%' % cambio_u.abs().mean())
print('monto, media absoluta:    %.2f%%' % cambio.abs().mean())
unidades, media absoluta: 9.55%
monto, media absoluta:    14.41%

Las unidades se mueven bastante menos que el dinero: 9,55% contra 14,41%.

Tiene sentido y vale la pena entender por qué. El monto de una venta depende de dos cosas que varían: cuántas unidades y a qué precio. Las unidades solo dependen de una. Cuando un número está hecho de dos números que bailan, baila más 💃

Esto tiene una consecuencia práctica: si quieres detectar un cambio de verdad en el negocio, mirar unidades te da menos ruido de fondo que mirar soles.

2. El mes más raro

Encuentra el mes con la caída más fuerte y el de la subida más fuerte, con su nombre, no solo el número.

print('peor:  ', cambio.idxmin(), '%.2f%%' % cambio.min())
print('mejor: ', cambio.idxmax(), '%.2f%%' % cambio.max())
peor:   2026-06 -32.36%
mejor:  2025-05 39.32%

Y aquí hay trampa, mírala bien 🕵️

El peor mes es junio de 2026, que es el último mes del archivo. Y arriba vimos que los datos terminan el 24 de junio. O sea que ese mes tiene 24 días y los demás tienen 30 o 31.

Esa caída del 32% no es una caída del negocio: es un mes al que le faltan seis días. Es de los errores más comunes que hay en informes de verdad, y no avisa de nada porque el cálculo está perfecto.

La regla: el último periodo de cualquier serie es sospechoso hasta que compruebes que está completo. Se comprueba mirando el máximo de la fecha, como hicimos, y si está incompleto se saca del análisis o se anota al lado.

3. Las cuentas sin el mes incompleto

Repite la medición de la variación quitando el último mes. ¿Cambia la conclusión sobre el 3%?

completos = mes.iloc[:-1]
c2 = (completos.pct_change() * 100).dropna()
print('con el mes cojo:  %.2f%%' % cambio.abs().mean())
print('sin el mes cojo:  %.2f%%' % c2.abs().mean())
print('peor mes ahora:   %.2f%%' % c2.min())
con el mes cojo:  14.41%
sin el mes cojo:  13.29%
peor mes ahora:   -23.09%

La media baja de 14,41% a 13,29% y el peor mes pasa de -32,36% a -23,09%.

O sea que sí, el mes incompleto estaba exagerando la variación. Pero la conclusión no se mueve ni un milímetro: 13,29% sigue siendo más de cuatro veces el 3% de la lámina.

Esto pasa a menudo y conviene tenerlo claro: encontrar un error no siempre cambia la respuesta. Se corrige igual, porque la próxima vez sí la cambiará.

4. ¿Y si el mes hubiera subido 12%?

Con lo medido, ¿a partir de qué subida empezarías a prestar atención? Escribe tu criterio con un número antes de mirar.

import numpy as np

for umbral in [3, 10, 15, 20, 30]:
    cuantos = (cambio.abs() >= umbral).sum()
    print('%2d%% o mas: %2d de %d meses (%.0f%%)'
          % (umbral, cuantos, len(cambio), 100 * cuantos / len(cambio)))
 3% o mas: 14 de 17 meses (82%)
10% o mas:  9 de 17 meses (53%)
15% o mas:  7 de 17 meses (41%)
20% o mas:  5 de 17 meses (29%)
30% o mas:  3 de 17 meses (18%)

Ahora se puede hablar con números en vez de con sensaciones 🙌

Una subida del 3% o más pasa en el 82% de los meses. O sea que celebrarla es celebrar casi cualquier mes.

Un movimiento del 30% o más pasa solo en el 18% de los meses. Ahí ya tienes un criterio defendible y que puedes escribir antes de mirar el dato: me llama la atención lo que pasa en menos de uno de cada cinco meses.

Guárdate esa idea porque es la del libro entero. Ese 18% del final es, en espíritu, lo mismo que un valor p: la probabilidad de ver algo así de grande si no está pasando nada especial. Lo formalizamos en el capítulo 10, pero el concepto es este y ya lo acabas de calcular tú 😍

5. La trampa de comparar contra el mejor mes

Alguien compara septiembre contra agosto y sale un +16%. Otro compara septiembre contra mayo. Calcula las dos y explica por qué la segunda es tramposa.

print('sep vs ago: %+.1f%%' % (100 * (mes['2025-09'] / mes['2025-08'] - 1)))
print('sep vs may: %+.1f%%' % (100 * (mes['2025-09'] / mes['2025-05'] - 1)))
sep vs ago: +16.0%
sep vs may: -11.6%

El mismo mes de septiembre sube 16,0% o baja 11,6% según con quién lo compares 🤯

No hay ningún error de cálculo en ninguna de las dos. Las dos son ciertas. Y por eso mismo esto es tan peligroso: quien elige la base elige el titular.

Agosto fue un mes flojo (había caído 14,7%), así que compararse contra él regala una subida. Mayo fue el mejor mes del año, así que compararse contra él regala una caída.

La defensa es fijar la comparación antes de mirar el resultado. Mes contra mes anterior, o mes contra el mismo mes del año pasado. Lo que no vale es elegir la base después de ver cuál queda mejor, que tiene nombre y sale en el capítulo 14: se llama p-hacking cuando se hace con pruebas, y es la misma familia de trampa.

6. Encuentra otra columna que sea puro ruido

Vimos que ciudad no separa nada y segmento sí. Prueba con canal y con categoria. ¿Cuál de las dos se parece más a ciudad?

for col in ['canal', 'categoria']:
    tasa = (v.groupby(col)['compro'].mean() * 100).round(1)
    print(col, '-> de %.1f a %.1f  (brecha %.1f puntos)'
          % (tasa.min(), tasa.max(), tasa.max() - tasa.min()))
    print(tasa.to_string())
    print()
canal -> de 46.1 a 65.2  (brecha 19.1 puntos)
canal
Marketplace    46.1
Tienda         61.9
Web            58.3
WhatsApp       65.2

categoria -> de 55.8 a 59.0  (brecha 3.2 puntos)
categoria
Abarrotes           59.0
Bebidas             57.5
Cuidado personal    55.8
Limpieza            58.9
Snacks              57.4

categoria es la que se parece a ciudad: 3,2 puntos entre la categoría que más cierra y la que menos, o sea nada.

canal no: 19,1 puntos. Marketplace cierra el 46,1% y WhatsApp el 65,2%. Eso es señal, aunque menos escandalosa que la de segmento.

Y aquí quiero que te quedes con el matiz que hace falta todo el libro: una brecha de 3,2 puntos podría ser real si hubiera muchísimos más datos. Lo que hace que sea ruido aquí no es el 3,2, es el 3,2 con 3.037 filas repartidas en cinco categorías. Con 300.000 filas, esa misma brecha sería una diferencia sólida.

El tamaño de la diferencia solo significa algo cuando lo miras junto a cuántos datos tienes. Esa idea es la mitad del libro, y la ponemos en fórmula en el capítulo 9.

Lo que te llevas

  • ❓ La estadística contesta una sola pregunta: ¿es real o es ruido? Todo lo demás es herramienta.
  • 📏 Antes de celebrar una diferencia, mide cuánto se mueven las cosas normalmente. Aquí el mes promedio se mueve 14,41%, así que un 3% es de los meses más planos que hay.
  • 🕳️ Los errores que no dan error son los peores. El atajo dayfirst=True cambió 847 fechas e inventó seis meses sin avisar.
  • 📅 El último periodo de una serie es sospechoso hasta que compruebes que está completo. Aquí junio tenía 24 días y fingía una caída del 32%.
  • 🎯 Quien elige la base de comparación elige el titular. Se fija antes de mirar el resultado.
  • ⚖️ Una diferencia solo significa algo junto a cuántos datos tienes. 3,2 puntos con 3.000 filas es ruido; con 300.000 sería señal.

Qué viene ahora

En el capítulo 2 arreglamos las cuatro Limas y ponemos cada columna en su tipo, que es lo que decide qué medidas puedes usar y cuáles no tienen sentido. Y sí, hay medidas que se calculan sin dar error y no significan absolutamente nada 🙃

¿Tienes alguna duda o consulta?