DEV Community

Cover image for ¿Cómo predice FORECAST.ETS el pico de ventas de diciembre?

¿Cómo predice FORECAST.ETS el pico de ventas de diciembre?

=FORECAST.ETS(...) parece magia: le das tres años de ventas mensuales y devuelve un diciembre más alto que cualquier otro mes, un enero que vuelve a bajar y un rango de confianza alrededor de ambos. Por debajo hay suavizado exponencial con tendencia y una estación que se repite, un método de finales de los años cincuenta que cabe en unas cuarenta líneas. La Forecast Sheet del editor de hojas de cálculo de la aplicación de escritorio osFoundry escribe esa función en una hoja nueva. Este artículo reconstruye la misma idea en Python, la ejecuta con los mismos 36 meses y comprueba en qué coincide con la hoja y dónde falla un método más simple. Los datos son un ejemplo: las ventas de una tienda pequeña con una subida constante, una forma anual fija y alrededor de un 4% de ruido, generadas para este recorrido.

Qué produce la hoja

Con 36 meses (de enero de 2023 a diciembre de 2025), Forecast Sheet con estacionalidad en auto y un nivel de confianza del 95% escribe tres columnas, Forecast, Lower y Upper Confidence Bound, y un gráfico. La celda C49, diciembre de 2026, contiene =FORECAST.ETS(A49,$B$2:$B$37,$A$2:$A$37,1,1) y muestra $20,654. Los dos límites de al lado son $19,008 y $22,300.

El gráfico de la hoja de previsión: historial en azul, previsión y rango de 2026

Esos son los números que hay que reproducir. Todo lo que sigue se ejecuta con uvx --with numpy --with scipy python -I forecast.py en Python 3.12, numpy 2.5.4 y scipy 1.18.1.

Primero, encontrar la estación

La estacionalidad en auto tiene que adivinar cuánto dura un ciclo. Una versión barata de esa conjetura es la autocorrelación del cambio de un mes al siguiente. Una tendencia suma lo mismo a cada mes y se cancela en las diferencias; una forma anual no.

d = np.diff(y); d = d - d.mean()
ac = [np.sum(d[:-k] * d[k:]) / np.sum(d * d) for k in range(1, 19)]
Enter fullscreen mode Exit fullscreen mode

En estos 36 meses, el retardo 12 sale con 0.53, el 6 con 0.06 y el 13 con -0.01, así que la longitud de la estación es 12. Es una regla práctica mía, no una descripción de cómo elige el editor su longitud de estación; lo que sí se puede comparar es el resultado, más abajo.

Holt-Winters aditivo

Con cada mes que llega se actualizan tres cosas: un nivel (dónde están las ventas ahora, sin la estación), una tendencia (cuánto crecen por mes) y doce valores de estación (cuánto por encima o por debajo del nivel está cada mes del calendario). Cada una es una mezcla ponderada de la observación nueva y la estimación anterior, con pesos alfa, beta y gamma.

def run(y, a, b, g):
    l, t, s = init(y); s = list(s); err = []
    for i, v in enumerate(y):
        err.append(v - (l + t + s[i % M]))              # one-step-ahead error
        ln = a * (v - s[i % M]) + (1 - a) * (l + t)     # level
        t = b * (ln - l) + (1 - b) * t                  # trend
        s[i % M] = g * (v - ln) + (1 - g) * s[i % M]    # season
        l = ln
    return np.array(err), l, t, s
Enter fullscreen mode Exit fullscreen mode

Los valores iniciales salen de los dos primeros años: la media del primer año como nivel, la diferencia entre las medias de los dos primeros años dividida entre doce como tendencia, y la desviación de cada mes respecto a la media del primer año como su estación. Los tres pesos se encuentran minimizando los errores cuadráticos a un paso con scipy.optimize.minimize, desde tres puntos de partida para que un mal arranque no decida la respuesta. Predecir h meses por delante es nivel + k * tendencia + estación[mes].

Para el intervalo usé una regla práctica: 1.96 veces la desviación típica de los residuos por la raíz cuadrada del horizonte. No se inspeccionó la fórmula de intervalo de la propia hoja, así que solo se comparan sus números.

Contra la hoja

Con los 36 meses completos, el ajuste da alfa 0.37, beta 0.01 y gamma 0.42, y una desviación típica de los residuos de $567. Frente a la columna Forecast de la hoja:

Mes Este script Forecast Sheet
Ene 2026 $12,136 $12,260
Dic 2026 $20,698 $20,654
Diferencia absoluta media, 12 meses $297 (2.0%)
Mayor diferencia (jun 2026) $587

Los doce meses coinciden en promedio con menos de un 2% de diferencia, lo que cabe esperar de dos implementaciones de la misma familia de métodos con distintos valores iniciales y distinto ajuste. No demuestra que el editor haga exactamente esto, y es un solo conjunto de datos con una forma limpia. Con datos reales, con un mes que falta o un cambio de nivel, las dos se separarán más.

Los intervalos no coinciden: diciembre sale aquí de $16,849 a $24,548, frente a $19,008 a $22,300 en la hoja. El mío es más ancho, y su ancho crece más rápido con el horizonte: $2,223 en enero a $7,700 en diciembre, frente a $1,621 y $3,292 en la hoja. La regla de la raíz cuadrada es tosca, y nada de esto dice qué banda está mejor calibrada.

Qué aporta la estación

Si se ejecutan los mismos datos sin estación, solo con nivel y tendencia (el método lineal de Holt), se ve lo que vale el patrón:

no season: Jan 17872   Dec 23159   residual sd 2185
Enter fullscreen mode Exit fullscreen mode

Sin la estación, la previsión de enero es $17,872, donde la hoja da $12,260 y los dos eneros anteriores cayeron cerca de un 40% respecto a diciembre. La desviación típica de los residuos es 2,185 frente a 567 con estación. La previsión de diciembre falla en el otro sentido, $23,159 frente a $20,654, porque una línea de tendencia no tiene forma de saber que diciembre es un pico y enero un valle.

Una prueba de un año

Se ajusta solo con enero de 2023 a diciembre de 2024, se predice 2025 y se compara con lo que pasó. Error porcentual absoluto medio (MAPE) de los doce meses:

Método MAPE Previsión dic 2025
Media global 19.4% $10,586
Último mes repetido 37.3% $17,000
Mismo mes del año pasado 12.0% $17,000
Mismo mes del año pasado por el crecimiento 4.1% $20,025
Holt-Winters (este script) 3.4% $18,511

En diciembre de 2025 se vendieron en realidad $19,480. De aquí salen dos cosas. Una copia estacional simple del año pasado (12.0%) no recoge el crecimiento, y al añadir el crecimiento anual observado, del 17.8% (4.1%), se queda casi a la par de Holt-Winters (3.4%); con dos años de datos, el suavizado exponencial no es un salto grande respecto a una fórmula cuidadosa de hoja de cálculo. Y la banda del 95% cubrió los doce valores reales, lo que con doce puntos solo dice que no es demasiado estrecha.

Es una sola partición de una sola serie de ejemplo. No dice nada sobre cómo se ordenan los métodos con otros datos.

Una trampa: pedirle a una biblioteca estadística que estime todo

El atajo obvio es statsmodels. Con el ajuste por defecto sobre los mismos 36 meses:

ETSModel(series, error='add', trend='add', seasonal='add', seasonal_periods=12).fit()
Enter fullscreen mode Exit fullscreen mode

statsmodels 0.15.0 da diciembre de 2026 como $22,738 con un rango de $17,688 a $27,787, un 10% por encima de la hoja. En la prueba de un año, el mismo modelo tuvo un MAPE del 33.2%, peor que repetir la media global. Probablemente, un modelo que estima doce valores iniciales de estación más los pesos de suavizado a partir de 24 o 36 puntos tiene demasiados números libres, y el ajuste acaba en algo raro. El script de arriba fija en cambio los valores iniciales a partir de los primeros años, una de las diferencias entre ambos; no se probó por separado. Conviene comparar cualquier previsión de una biblioteca con "mismo mes del año pasado por el crecimiento" antes de fiarse de ella.

Para usarlo

El script (forecast.py) es todo el experimento. Para cifras de ventas reales, el camino más rápido es la Forecast Sheet de la aplicación, que es gratuita en la página de descarga de osFoundry y trabaja sobre el archivo del ordenador. Escribir el método a mano sirve para saber qué significa el rango y para notar cuándo hay que descartar una previsión.

Top comments (0)