Saltar al contenido

lección 7

Estacionalidad, tendencia y calendario: comparar como es debido

Separar los tres efectos que mueven una serie de negocio, y cuándo la comparación no se puede hacer.

55 min

¿Venderías más helados en agosto que en enero y dirías que tu negocio está creciendo? Suena absurdo, pero es exactamente lo que hacen miles de informes corporativos cada mes cuando comparan diciembre contra noviembre y celebran un +22 % que no es crecimiento: es Navidad. El calendario tiene un efecto brutal sobre las ventas, el tráfico web, las llamadas al soporte y prácticamente cualquier métrica que midas. Ignorarlo es confundir la marea con un tsunami. Y la pregunta que separa un informe útil de uno engañoso no es «vendimos más que el mes pasado», sino «vendimos más que en el mismo mes del año pasado, ajustando por lo que el calendario siempre hace». Esa distinción, tan fácil de decir como difícil de calcular bien, es el tema de esta lección.

Cada serie temporal de negocio lleva dentro tres fuerzas que se mezclan: la tendencia (la dirección a largo plazo: estamos creciendo o encogiéndonos), la estacionalidad (patrones que se repiten en el mismo periodo cada año, cada semana o cada día), y el ruido (lo que no es ninguna de las dos anteriores). Separar las tres es lo que distingue un informe que informa de uno que engaña. Esta lección te enseña a hacer esa separación con SQL, sin salir de DuckDB, usando dos herramientas: la comparación con el mismo periodo del año anterior y la media móvil que ya conoces de la lección de frames.

### Las tres fuerzas de una serie temporal

Piensa en la temperatura de tu ciudad. Hay una tendencia a largo plazo (el cambio climático: la media de cada década sube). Hay una estacionalidad (en julio hace calor, en enero frío, y eso se repite CADA año). Y hay ruido (un martes de abril puede hacer 5 grados más que el miércoles, sin razón). Nadie diría "abril fue más cálido que enero, estamos en calentamiento global" — sería absurdo, porque eso es estacionalidad, no tendencia. Pero en ventas cometemos ese error constantemente.

Las tres fuerzas que mueven cualquier serie de negocio. Tu trabajo: aislarlas.

En 1954, el Bureau of Labor Statistics de Estados Unidos empezó a publicar sus cifras de empleo dos veces: la bruta y la desestacionalizada. El motivo era que los datos de enero siempre parecían catastróficos —las contrataciones temporales de Navidad terminaban y la cifra caía en picado— y los titulares anunciaban una crisis cada enero. Setenta años después, las empresas siguen cayendo en la misma trampa con sus propios datos.

### La estacionalidad: el patrón que se repite

Estacionalidad no es solo "Navidad vende más". Hay estacionalidad semanal (los lunes se vende menos que los viernes en hostelería), mensual (la primera semana del mes se cobra y se gasta más), anual (verano es bajo en B2B, alto en turismo). Cualquier patrón predecible que se repite en el mismo momento del ciclo es estacionalidad. La clave para identificarla: si puedes decir "esto pasa SIEMPRE en [periodo]", es estacional.

  • Estacionalidad semanal: restaurantes venden más el viernes y sábado, gimnasios los lunes.
  • Estacionalidad mensual: la primera quincena gasta más (cobro de nómina), la segunda frena.
  • Estacionalidad anual: jugueterías en diciembre, heladerías en julio, contabilidad en enero (cierre fiscal).
  • Estacionalidad de evento: el Prime Day de Amazon, el Singles Day (11.11) en China. No es una fecha fija del calendario civil, pero se repite cada año.

Error clásico: "Las ventas han subido un 30% este mes." Sin decir respecto a qué. Si es respecto al mes anterior y ese mes era febrero (el más corto del año, sin festivos comerciales), el 30% puede ser pura estacionalidad. Siempre pregunta: ¿comparado con qué?, y ¿tiene sentido esa comparación?

### La comparación correcta: mismo periodo del año anterior (YoY)

La forma más limpia de neutralizar la estacionalidad es comparar cada periodo con el mismo periodo del año anterior. Diciembre 2024 contra diciembre 2023. Semana 12 de 2024 contra semana 12 de 2023. El lunes 4 de marzo contra el lunes equivalente del año pasado. Si los dos periodos comparten la misma estacionalidad, la diferencia que queda ES la tendencia (más el ruido, que es pequeño si tomas periodos de al menos una semana). Ya conoces LAG — ahora lo usamos con un desfase de 12 meses en lugar de 1.

1-- Comparacion YoY (Year over Year) por mes
2WITH mensual AS (
3 SELECT
4 DATE_TRUNC('month', fecha) AS mes,
5 SUM(importe) AS ventas
6 FROM pedidos
7 WHERE estado = 'completado'
8 GROUP BY DATE_TRUNC('month', fecha)
9)
10SELECT
11 mes,
12 ventas,
13 LAG(ventas, 12) OVER (ORDER BY mes) AS ventas_ano_anterior,
14 ROUND(
15 100.0 * (ventas - LAG(ventas, 12) OVER (ORDER BY mes))
16 / LAG(ventas, 12) OVER (ORDER BY mes), 1
17 ) AS yoy_pct
18FROM mensual
19ORDER BY mes;

LAG(ventas, 12) = "las ventas de hace 12 meses", es decir, el mismo mes del año pasado.

Consejo de senior: en un dashboard, muestra SIEMPRE la variación YoY junto al MoM (mes contra mes anterior). El MoM da la reacción rápida ("este mes fue peor que el anterior"), el YoY da la perspectiva ("pero mejor que el mismo mes del año pasado"). Los dos juntos cuentan la historia completa. Un CEO experimentado desconfía de un informe que solo muestre uno de los dos.

### La media móvil: ver la tendencia sin el ruido

Ya viste la media móvil de 7 días en la lección de frames. Ahora la vas a usar con intención: no como "una forma bonita de suavizar", sino como la herramienta que separa la tendencia del ruido estacional. Una media móvil de 7 días elimina el efecto del día de la semana (porque siempre promedia un ciclo semanal completo). Una de 30 días reduce el efecto mensual. Y una de 365 días — o su equivalente con datos mensuales, 12 meses — elimina prácticamente toda la estacionalidad anual y te deja la tendencia pura.

1-- Media movil de 12 meses: la tendencia pura
2WITH mensual AS (
3 SELECT
4 DATE_TRUNC('month', fecha) AS mes,
5 SUM(importe) AS ventas
6 FROM pedidos
7 WHERE estado = 'completado'
8 GROUP BY DATE_TRUNC('month', fecha)
9)
10SELECT
11 mes,
12 ventas,
13 ROUND(
14 AVG(ventas) OVER (
15 ORDER BY mes
16 ROWS BETWEEN 11 PRECEDING AND CURRENT ROW
17 ), 0
18 ) AS media_movil_12m
19FROM mensual
20ORDER BY mes;

La media móvil de 12 meses suaviza toda la estacionalidad anual: lo que queda es la tendencia.

La línea gris sube y baja con las estaciones. La verde te dice si de verdad creces.

### La tabla de calendario: dim_calendar

Hasta aquí has neutralizado la estacionalidad con LAG y media móvil. Pero hay un problema que esas herramientas no resuelven: no todos los meses tienen los mismos días laborables. Marzo de 2024 tuvo 19 días laborables (21 de diario menos el Jueves y el Viernes Santo). Marzo de 2023 tuvo 23 (Semana Santa cayó en abril). Si comparas "ventas de marzo" sin ajustar, parece que 2024 vendió menos — pero es que tuvo cuatro días menos de tienda abierta. Para resolver esto necesitas una tabla de calendario, también llamada dim_calendar o dim_date.

Una tabla de calendario es una tabla con una fila por cada día (normalmente de los últimos 5-10 años y los próximos 2-3) y columnas que describen ese día: si es laborable o festivo, a qué semana pertenece, qué trimestre, si es fin de semana, si cae en Semana Santa, si es Black Friday. Es una dimensión más de tu modelo de datos, y probablemente la más útil que existe. En la mayoría de empresas ya está construida — tu trabajo como analista es saber que existe y usarla.

1-- Construir una dim_calendar basica en DuckDB
2CREATE OR REPLACE TABLE dim_calendar AS
3WITH fechas AS (
4 SELECT UNNEST(
5 generate_series(DATE '2022-01-01', DATE '2025-12-31', INTERVAL 1 DAY)
6 )::DATE AS fecha
7)
8SELECT
9 fecha,
10 YEAR(fecha) AS anio,
11 MONTH(fecha) AS mes,
12 DAY(fecha) AS dia,
13 DAYOFWEEK(fecha) AS dia_semana, -- 0=domingo, 6=sabado
14 WEEKOFYEAR(fecha) AS semana_iso,
15 QUARTER(fecha) AS trimestre,
16 CASE
17 WHEN DAYOFWEEK(fecha) IN (0, 6) THEN false
18 ELSE true
19 END AS es_laborable,
20 CASE
21 WHEN DAYOFWEEK(fecha) IN (0, 6) THEN 'fin_de_semana'
22 ELSE 'laborable'
23 END AS tipo_dia
24FROM fechas;

Una dim_calendar mínima: una fila por día con sus atributos temporales.

El Viernes Santo parece un viernes normal sin la tabla de calendario.

Consejo de senior: en tu primera semana en una empresa nueva, pregunta "¿dónde está la tabla de calendario y qué festivos tiene cargados?". Si no existe, propón crearla — son 50 líneas de SQL y ahorra decenas de horas de errores al año. Si existe pero solo tiene festivos nacionales, pregunta si faltan los autonómicos (en España, cada comunidad tiene los suyos y un e-commerce con almacén en Madrid y otro en Barcelona tiene días de envío distintos).

### Usar dim_calendar para comparar periodos equivalentes

Con la tabla de calendario puedes hacer comparaciones que antes eran imposibles o muy frágiles. El ejemplo más claro: "ventas por día laborable". En vez de comparar marzo contra marzo y confundir un puente con una caída de demanda, comparas los 19 días laborables de marzo de 2024 contra los 23 de marzo de 2023, normalizando a "ventas por día laborable". Así el efecto de la Semana Santa desaparece.

1-- Ventas por dia laborable: neutralizar el efecto calendario
2WITH ventas_mes AS (
3 SELECT
4 DATE_TRUNC('month', p.fecha) AS mes,
5 SUM(p.importe) AS ventas_totales,
6 COUNT(DISTINCT CASE WHEN c.es_laborable THEN p.fecha END) AS dias_laborables
7 FROM pedidos p
8 JOIN dim_calendar c ON c.fecha = p.fecha
9 WHERE p.estado = 'completado'
10 GROUP BY DATE_TRUNC('month', p.fecha)
11)
12SELECT
13 mes,
14 ventas_totales,
15 dias_laborables,
16 ROUND(ventas_totales / dias_laborables, 0) AS ventas_por_dia_laborable,
17 LAG(ROUND(ventas_totales / dias_laborables, 0), 12)
18 OVER (ORDER BY mes) AS vpdl_ano_anterior,
19 ROUND(
20 100.0 * (ventas_totales / dias_laborables
21 - LAG(ventas_totales / dias_laborables, 12) OVER (ORDER BY mes))
22 / LAG(ventas_totales / dias_laborables, 12) OVER (ORDER BY mes), 1
23 ) AS yoy_ajustado_pct
24FROM ventas_mes
25ORDER BY mes;

Dividir por días laborables antes de comparar: la comparación honesta.

### Cuándo la comparación no se puede hacer

No siempre puedes comparar con el año anterior. Hay situaciones donde la comparación YoY es directamente engañosa o imposible:

  1. 01.El negocio no existía hace un año. Una startup de 8 meses no tiene YoY. Usa WoW (semana contra semana) o MoM con mucho cuidado.
  2. 02.Hubo un evento irrepetible. 2020 y 2021 están contaminados por la pandemia. Comparar 2022 contra 2021 da crecimientos de +300% en hostelería que no significan nada. Algunos equipos usan 2019 como base ("vs pre-pandemia").
  3. 03.Cambiaste el producto radicalmente. Si en julio lanzaste una línea nueva que duplicó el catálogo, el YoY de agosto mezcla crecimiento orgánico con "hay el doble de cosas que comprar". No es comparable.
  4. 04.Semana Santa móvil. Cae entre marzo y abril, pero no el mismo día cada año. Comparar "marzo 2024" contra "marzo 2023" es engañoso si uno tuvo Semana Santa y el otro no. La solución: comparar periodos con el mismo número de días laborables, no meses naturales.
  5. 05.El Black Friday no cae en la misma semana. Hay años donde cae el 24 de noviembre y otros donde cae el 29. Si comparas "semana 47" puede que un año lo incluya y el otro no.

Regla de oro de las comparaciones temporales: antes de calcular un porcentaje de variación, pregúntate "¿los dos periodos que estoy comparando son realmente equivalentes?". Si uno tuvo un evento que el otro no (una campaña, un festivo, un lanzamiento), la comparación está rota y el número que salga es basura útil — parece un dato pero no dice nada.

### Ejemplo guiado: separar tendencia de estacionalidad

Vamos a montar un ejemplo completo. Tenemos dos años de ventas mensuales y queremos responder a la directora financiera: "¿estamos creciendo de verdad, o solo parece por la estacionalidad?". Vamos a calcular tres cosas: las ventas brutas, la variación YoY (neutraliza estacionalidad), y la media móvil de 12 meses (muestra la tendencia).

1-- Ejemplo completo: ventas brutas + YoY + tendencia
2WITH datos AS (
3 SELECT * FROM (VALUES
4 ('2023-01-01'::DATE, 80), ('2023-02-01'::DATE, 75),
5 ('2023-03-01'::DATE, 90), ('2023-04-01'::DATE, 85),
6 ('2023-05-01'::DATE, 95), ('2023-06-01'::DATE, 100),
7 ('2023-07-01'::DATE, 70), ('2023-08-01'::DATE, 65),
8 ('2023-09-01'::DATE, 105), ('2023-10-01'::DATE, 110),
9 ('2023-11-01'::DATE, 130), ('2023-12-01'::DATE, 150),
10 ('2024-01-01'::DATE, 88), ('2024-02-01'::DATE, 82),
11 ('2024-03-01'::DATE, 99), ('2024-04-01'::DATE, 92),
12 ('2024-05-01'::DATE, 103), ('2024-06-01'::DATE, 108),
13 ('2024-07-01'::DATE, 76), ('2024-08-01'::DATE, 71),
14 ('2024-09-01'::DATE, 115), ('2024-10-01'::DATE, 120),
15 ('2024-11-01'::DATE, 142), ('2024-12-01'::DATE, 163)
16 ) AS t(mes, ventas)
17)
18SELECT
19 mes,
20 ventas,
21 LAG(ventas, 12) OVER (ORDER BY mes) AS ventas_yoy,
22 ROUND(
23 100.0 * (ventas - LAG(ventas, 12) OVER (ORDER BY mes))
24 / LAG(ventas, 12) OVER (ORDER BY mes), 1
25 ) AS variacion_yoy_pct,
26 ROUND(
27 AVG(ventas) OVER (
28 ORDER BY mes
29 ROWS BETWEEN 11 PRECEDING AND CURRENT ROW
30 ), 1
31 ) AS media_movil_12m
32FROM datos
33ORDER BY mes;

Julio siempre cae (vacaciones) y diciembre siempre sube (Navidad). Pero el YoY y la media móvil revelan un crecimiento real del ~8-10% anual.

### Días laborables vs festivos: por qué el lunes de Semana Santa no es un lunes normal

Un error sutil pero frecuente: tratar todos los lunes como iguales. "Las ventas de los lunes bajan un 15% respecto al viernes." Vale, pero el Lunes de Pascua es festivo en Cataluña, la Comunidad Valenciana, Baleares, Castilla-La Mancha, País Vasco, Navarra y La Rioja — ese lunes no baja un 15%, baja un 80% porque la tienda está cerrada (o un 200% arriba si eres turismo). Y de toda la Semana Santa, el único festivo en toda España es el Viernes Santo; el Jueves Santo es festivo en casi todas las comunidades menos Cataluña y la Comunidad Valenciana. Sin la tabla de calendario, esos días festivos contaminan tu media de "días normales" y distorsionan cualquier modelo de previsión.

La regla práctica: cualquier análisis que agrupe por día de la semana necesita excluir (o marcar aparte) los festivos. Y cualquier comparación "semana contra semana" necesita comprobar que las dos semanas tienen el mismo número de días laborables. Una semana con un puente y una semana normal NO son comparables sin ajuste.

1-- Patron de ventas por dia de la semana, excluyendo festivos
2SELECT
3 c.dia_semana,
4 CASE c.dia_semana
5 WHEN 1 THEN 'Lunes' WHEN 2 THEN 'Martes'
6 WHEN 3 THEN 'Miercoles' WHEN 4 THEN 'Jueves'
7 WHEN 5 THEN 'Viernes'
8 END AS nombre_dia,
9 COUNT(*) AS num_dias,
10 ROUND(AVG(diario.ventas), 0) AS media_ventas
11FROM (
12 SELECT fecha, SUM(importe) AS ventas
13 FROM pedidos
14 WHERE estado = 'completado'
15 GROUP BY fecha
16) diario
17JOIN dim_calendar c ON c.fecha = diario.fecha
18WHERE c.es_laborable = true -- Solo dias laborables reales
19GROUP BY c.dia_semana
20ORDER BY c.dia_semana;

Filtrar por es_laborable = true excluye festivos que caen en día de diario.

Esto te lo van a preguntar en la entrevista: "Las ventas del último mes bajaron un 5% respecto al anterior. ¿Qué investigarías antes de alarmarte?" La respuesta que quieren oír: 1) cuántos días laborables tuvo cada mes, 2) hubo algún festivo o puente que el mes anterior no tenía, 3) la comparación correcta es YoY, no MoM, para ese tipo de conclusión. Eso demuestra que piensas antes de reaccionar.

### Cómo NO engañarse: un diciembre siempre vende más

Volvamos al principio: el director comercial que celebra "+22% en diciembre vs noviembre". El problema no es la matemática (22% está bien calculado), es la pregunta. La pregunta implícita es "¿lo estamos haciendo mejor?" y la respuesta es "no lo sabemos, porque diciembre siempre vende más que noviembre". Para saber si lo estamos haciendo mejor necesitamos una de estas tres cosas:

  1. 01.Comparar con el mismo mes del año anterior (YoY): diciembre 2024 vs diciembre 2023. Si crece, crecimiento real.
  2. 02.Comparar con el presupuesto/objetivo de diciembre: el equipo de planificación fijó una meta que YA incluía la estacionalidad esperada. Si la superas, rendimiento extra.
  3. 03.Mirar la media móvil: si la línea de tendencia sigue subiendo en diciembre, el negocio crece. Si se aplana o baja, diciembre "salvó el trimestre" pero la inercia es negativa.

Y hay una cuarta trampa más dañina: el "efecto base" estacional. Si diciembre 2023 fue anormalmente malo (huelga de transportes, competidor con superoferta), entonces diciembre 2024 "normal" parece un crecimiento espectacular. Un YoY de +40% que en realidad es un "volver a la normalidad". Por eso el YoY se lee SIEMPRE mirando también la base: crecimos un 40%... respecto a un mes que fue un desastre. El crecimiento real es mucho menor.

Tres lecturas del mismo diciembre. Solo la segunda y la tercera juntas dicen la verdad.

### Resumen: el checklist antes de presentar una variación

  1. 01.¿Los dos periodos tienen la misma estacionalidad? (mismo mes, misma semana del año, mismo tipo de día)
  2. 02.¿Los dos periodos tienen el mismo número de días laborables?
  3. 03.¿Hubo algún evento extraordinario en alguno de los dos? (campaña, festivo móvil, incidencia)
  4. 04.¿La base del año anterior era "normal" o fue anómala? (efecto base)
  5. 05.¿Estoy mostrando la variación YoY junto al MoM para que se vea la perspectiva?

Si alguna de esas cinco preguntas da "no" o "no lo sé", el número que vas a presentar necesita una nota al pie. No hace falta que el número sea perfecto — hace falta que quien lo lea sepa qué limitaciones tiene. Un "+15% YoY, sin ajustar por el puente de mayo que este año cayó en jueves" es honesto. Un "+15% YoY" sin más es un número que dice más de lo que sabe.

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...