VBA Estacionalidad
Sabemos que una serie de tiempo puede descomponerse en términos de su nivel, tendencia, estacionalidad y un ruido residual, idealmente blanco.
Si bien es fácil obtener estadígrafos para la media y la pendiente asociada a la recta tendencia, no existe una función en Excel para obtener los coeficientes de estacionalidad en forma directa. El objetivo de este artículo es desarrollar un algoritmo para automatizar este cálculo a partir de una serie de datos cualquiera.
Un caso típico sería contar con una serie de datos mensuales y obtener un coeficiente de estacionalidad para cada uno de los meses del año (12 en total). Podría ocurrir, sin embargo, que tuviésemos datos trimestrales y necesitemos coeficientes para cada uno de los trimestres (4 en total). Denominaremos periodo al número de coeficientes de estacionalidad que deseamos calcular.
Los datos reales con los que contamos podrían no comenzar exactamente junto con un nuevo periodo (un mes distinto a enero, un trimestre distinto al primero). Podríamos generalizar entonces el problema a una serie de datos de periodo m cuyo primer dato corresponde a Inicio, con Inicio entre 1 y m.
En el siguiente ejemplo se tiene una serie de datos de periodo m=12 y cuyos datos reales tienen su Inicio en el mes 7. Los datos corresponden a las ventas de un determinado producto entre los meses de julio 2016 y junio 2019 (n=36).
Comenzaremos creando en VBA una función que llamaremos COEFICIENTES_ESTACIONALIDAD, que recibirá una serie de datos (como un rango de Excel) y las variables Inicio y Periodo (m). Nos devolverá un arreglo de m datos con los coeficientes de estacionalidad normalizados desde 1 hasta m.
Public Function COEFICIENTES_ESTACIONALIDAD(rngDatos As Range, _
intPeriodo As Integer, Optional intInicio As Integer) As Variant
'Calcula coeficiente de estacionalidad de un rango de datos
'y los devuelve en un arreglo de intPeriodo (m) valores
'considerando que el nuevo periodo se inicia en el dato intInicio
Dim arrDatos As Variant
Dim arrDatosSuavizados As Variant
Dim arrRatios As Variant
Dim arrCoeficientes As Variant
'Si no se especifica intinicio, se asume que es igual a 1
If intInicio = 0 Then intInicio = 1
'Leer los datos rngDatos -> arrDatos (n datos)
arrDatos = LEERDATOS(rngDatos)
'Calcular promedios moviles arrDatos -> arrDatosSuavizados (n-m)
arrDatosSuavizados = PROMEDIOMOVIL(arrDatos, intPeriodo)
'Calcular ratios arrDatosSuavizados (n-m) -> arrRatios (n-m)
arrRatios = CALCULARRATIOS(arrDatos, arrDatosSuavizados, intPeriodo)
'Calcular mediana por mes arrRatios (n-m)
arrCoeficientes = MEDIASPERIODICAS(arrRatios, intPeriodo)
'Reordenar arreglo de coeficientes arrCoeficientes(m)
arrCoeficientes = REORDENARARREGLO(arrCoeficientes, intInicio)
'Normalizar arreglo de coeficientes arrCoeficientes(m)
arrCoeficientes = NORMALIZARARREGLO(arrCoeficientes)
'Devolver coeficientes de estacionalidad
COEFICIENTES_ESTACIONALIDAD = arrCoeficientes
End Function
El algoritmo comienza leyendo los datos en el arreglo arrDatos y obteniendo una versión suavizada de los n datos: un arreglo que tendrá m datos menos, dado que utilizaremos para ello un promedio móvil de orden m. Vale decir, perderemos un periodo de datos, por lo que es imprescindible contar con datos suficientes: mínimo 2 periodos, idealmente 3 o más.
Private Function LEERDATOS(ByVal rngDatos As Range) As Variant
'Devuelve un arreglo de 1 hasta rngDatos.count datos
Dim arrAux() As Double
Dim i As Integer
Dim rngCelda As Range
i = 1
ReDim arrAux(1 To rngDatos.Count)
For Each rngCelda In rngDatos
arrAux(i) = rngCelda.Value
i = i + 1
Next rngCelda
LEERDATOS = arrAux
End Function
Private Function PROMEDIOMOVIL(ByVal arrDatos As Variant, _
ByVal intPeriodo As Integer) As Variant
'Calcula el promedio móvil de intPeriodo datos
Dim intCont As Integer
Dim arrAux() As Double
Dim dblSuma As Double
Dim i, j As Integer
intCont = UBound(arrDatos) - intPeriodo
ReDim arrAux(1 To (UBound(arrDatos) - intPeriodo))
For i = 1 To intCont
dblSuma = 0
For j = (i + 1) To (i + intPeriodo)
dblSuma = dblSuma + arrDatos(j)
Next j
arrAux(i) = dblSuma / intPeriodo
Next i
PROMEDIOMOVIL = arrAux()
End Function
La figura muestra los valores del arreglo desestacionalizado resultante. Como hemos usado los m primeros datos para calcular el promedio móvil del periodo siguiente, el arreglo resultante posee n-m datos.
Ahora podemos obtener los candidatos a coeficientes dividiendo los n-m datos reales correspondientes por cada uno de los n-m datos suavizados que acabamos de calcular. El arreglo resultante tendrá también n-m datos. Programamos este cálculo en la función CALCULARRATIOS.
Private Function CALCULARRATIOS(ByVal arrDatos As Variant, _
ByVal arrDatosSuavizados As Variant, ByVal intPeriodo As Integer) _
As Variant
'Calcula los ratios entre los datos reales y la serie desestacionalizada
Dim arrAux() As Double
Dim i As Integer
ReDim arrAux(LBound(arrDatosSuavizados) To UBound(arrDatosSuavizados))
For i = LBound(arrDatosSuavizados) To UBound(arrDatosSuavizados)
arrAux(i) = arrDatos(i + intPeriodo) / arrDatosSuavizados(i)
Next i
CALCULARRATIOS = arrAux()
End Function
Para pasar de n-m cocientes a los m coeficientes requeridos, promediamos los cocientes de los distintos periodos. En nuestro ejemplo, el coeficiente de julio lo calcularemos como el promedio de los cocientes correspondientes al mes de julio, para los años 2017, 2018 y 2019.
Private Function MEDIASPERIODICAS(ByVal arrDatos As Variant, _
ByVal intPeriodo As Integer) As Variant
'Calcula las intPeriodo medias de los datos del arreglo
Dim arrAux() As Double
Dim dblSuma As Double
Dim i, j, k As Integer
ReDim arrAux(1 To intPeriodo) As Double
For i = 1 To intPeriodo
dblSuma = 0
k = 0
For j = i To UBound(arrDatos) Step intPeriodo
k = k + 1
dblSuma = dblSuma + arrDatos(j)
Next j
arrAux(i) = dblSuma / k
Next i
MEDIASPERIODICAS = arrAux()
End Function
Si el mes Inicio es distinto a 1, como en este caso, los coeficientes calculados se encontrarán desordenados. Iniciarán en el coeficiente 7, seguirán hasta el 12 y luego continuarán en el 1 hasta el 6. Creamos una función que nos permita reordenarlos, teniendo como parámetro el mes Inicio.
Private Function REORDENARARREGLO(ByVal arrDatos As Variant, _
ByVal intPosicion As Integer) As Variant
'Reordena un arreglo que comienza en la
Dim arrAux() As Double
Dim i As Integer
Dim n As Integer
n = UBound(arrDatos) - LBound(arrDatos) + 1
ReDim arrAux(1 To n) As Double
For i = 1 To n
If i < intPosicion Then
arrAux(i) = arrDatos(i + intPosicion - 1)
Else
arrAux(i) = arrDatos(i - intPosicion + 1)
End If
Next i
REORDENARARREGLO = arrAux()
End Function
Por último, nos encargamos de normalizar los coeficientes, para que su promedio sea uno. Para ello dividimos cada coeficiente por la sumatoria de ellos y volvemos a multiplicar por el número de coeficientes:
Private Function NORMALIZARARREGLO(ByVal arrDatos As Variant) As Variant
'Normaliza el arreglo para que su media sea exactamente 1
Dim arrAux() As Double
Dim dblSuma As Double
Dim i As Integer
Dim n As Integer
dblSuma = 0
n = UBound(arrDatos) - LBound(arrDatos) + 1
ReDim arrAux(1 To n) As Double
'Primero se suman todos los elementos del arreglo
For i = 1 To n
dblSuma = dblSuma + arrDatos(i)
Next i
'Se divide por la suma de los elementos para normalizar
For i = 1 To n
arrAux(i) = arrDatos(i) / dblSuma * n
Next i
NORMALIZARARREGLO = arrAux()
End Function
De esta forma, hemos creado una función vectorial que nos permitirá calcular de una vez los m coeficientes de estacionalidad de una serie de datos.
¿Cómo saber si los coeficientes de estacionalidad calculados son significativos? Podríamos desestacionalizar la serie original (usando los coeficientes resultantes) y calcular qué fracción de la volatilidad ha sido reducida.
Calcularemos para ello SSE (suma de errores cuadráticos de los valores desestacionalizados y SST (suma de errores cuadráticos de los valores originales) . El cociente entre ambos valores es una medida del ajuste de nuestro modelo, denominado Coeficiente de Determinación o R2.
Public Function AJUSTE_ESTACIONALIDAD_R2(rngDatos As Range, _
rngCoeficientes As Range, intInicio As Integer)
Dim arrDatos() As Double
Dim arrCoeficientes() As Double
Dim arrDesestacionalizados() As Double
Dim dblSSEdatos As Double
Dim dblSSEdesestacionalizados As Double
'Leer arrDatos
arrDatos = LEERDATOS(rngDatos)
'Leer arrCoeficientes
arrCoeficientes = LEERDATOS(rngCoeficientes)
'Desestacionalizar los datos usando los coeficientes
arrDesestacionalizados = DESESTACIONALIZAR(arrDatos, _
arrCoeficientes, intInicio)
'Calcular SSE (suma de los errores cuadráticos) de los datos
dblSSEdatos = SSE(arrDatos)
'Calcular SSE de los datos desestacionalizados
dblSSEdesestacionalizados = SSE(arrDesestacionalizados)
'Calcular R2 del ajuste de estacionalidad
AJUSTE_ESTACIONALIDAD_R2 = 1 - dblSSEdesestacionalizados / dblSSEdatos
End Function
Private Function DESESTACIONALIZAR(ByVal arrDatos As Variant, _
ByVal arrCoeficientes As Variant, ByVal intInicio As Integer) As Variant
'Divide cada dato por el coeficiente de estacionalidad correspondiente
'según el parámetro intInicio. El primer índice de arrCoeficientes es 1
Dim arrAux() As Double
Dim i, n, m, k As Integer
n = UBound(arrDatos) - LBound(arrDatos) + 1
m = UBound(arrCoeficientes)
ReDim arrAux(LBound(arrDatos) To UBound(arrDatos)) As Double
For i = LBound(arrDatos) To UBound(arrDatos)
'Se calcula el indice k del coeficiente que corresponde al dato i
k = (i + intInicio - 2) Mod m + 1
'Revisión de bordes:
'si intinicio = 1 y i=1, k = (1+1-2) mod m + 1 = 0 + 1 = 1 (ok)
'si intinicio = 2 y i=m, k = (m+2-2) mod m + 1 = m mod m + 1 = 0 + 1
'= 1 (ok)
'Se divide el dato por el coeficiente correspondiente
arrAux(i) = arrDatos(i) / arrCoeficientes(k)
Next i
DESESTACIONALIZAR = arrAux
End Function
Private Function SSE(ByVal arrDatos As Variant) As Double
'Entrega la suma de errores cuadráticos respecto de la media
Dim dblMedia As Double
Dim dblSuma As Double
Dim i As Integer
'Calcular la media de los datos
dblMedia = Application.WorksheetFunction.Average(arrDatos)
'Calculamos la distancia cuadrática respecto de la media para cada dato
dblSuma = 0
For i = LBound(arrDatos) To UBound(arrDatos)
dblSuma = dblSuma + (arrDatos(i) - dblMedia) ^ 2
Next i
SSE = dblSuma
End Function
El valor de R2 va entre 0 y 1, representando en este caso el porcentaje del error cuadrático de la serie que puede ser explicado por los coeficientes de estacionalidad calculados. Mientras más cercano a 1, mayor significancia del modelo. Si la serie posee estacionalidad, esperaríamos encontrar R2 cercanos o superiores a 0,5. Es importante realizar esta verificación, puesto que nuestro algoritmo siempre encontrará coeficientes de estacionalidad, sin importar que estos valores den cuenta o no de la presencia de estacionalidad en los datos.
Y bueno, hasta aquí la entrega de hoy. ¿Qué nuevas mejoras o aplicaciones se te vienen en mente? Nos vemos pronto.