The world’s leading publication for data science, AI, and ML professionals.

Elevate Your Business Analytics: Step-by-Step Guide To Seasonal Adjustment

We all understand the importance of breaking time series into its components for forecasting, but the same is not emphasized enough in…

We all understand the importance of breaking time series into its components for forecasting, but the same is not emphasized enough in business performance analysis.

As a business performance analyst, I constantly report monthly revenue performance and track business cycle trends. To deal with the problem of seasonal changes, I rely on year-over-year comparisons. The problem is that these comparisons rely on 12-month-old data, which means that you will catch up with the trend late, which could have devastating consequences. Economists and statisticians have a more sophisticated way of dealing with seasonal fluctuation and catching changes in the business cycle soon after they occur.

Economists decompose macroeconomic data to report seasonally adjusted data and rely on month-over-month (or quarter-over-quarter) changes in seasonally adjusted metrics for a timely view of economic activity.

Photo by Stephen Dawson on Unsplash
Photo by Stephen Dawson on Unsplash

You don’t have to become a statistician or economist to stay on top of your business trends. The US Census Bureau made their X-13ARIMA-SEATS seasonal adjustment software available to the public, and here is how you can leverage it in Python to elevate your business analytics.

Download X 13 ARIMA SEATS

You can leverage Statsmodels X13_arima_analysis, a Python wrapper, to adjust your business data for seasonal fluctuations.

First, you will need to download the X-13ARIMA-SEATS executable from the Census website.

The last version – build 60 (at the time of writing) did not work for me, so I downloaded the previous version – build 59.

Once downloaded, you can unzip the file in your folder of choice.

After unzipping, you should get a folder like this. (image by author)
After unzipping, you should get a folder like this. (image by author)

Set up your Python notebook.

Apart from importing your normal packages for Data Analysis, you will need to set the environmental variable X13PATH to the path of the unzipped folder. If you skip this step, you will get an error when running your analysis.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from statsmodels.tsa.x13 import x13_arima_analysis

from datetime import datetime
from dateutil.relativedelta import relativedelta
import os

# Set the X13PATH environmental variable to the folder where you unzipped the X-13 executables
os.environ['X13PATH'] = r"C:...x13as_ascii-v1-1-b59x13as"

Import & prepare your data

For this example, I am using publicly reported quarterly Tesla revenue.

To run X13_arima_analysis, you will need a minimum of 3 years of data for the model to capture the seasonality patterns. Your data should be either monthly or quarterly. You will need to set the date column as the index of your Data Frame and ensure you specify the frequency.

#load data
df = pd.read_excel("TSLA_Revenue.xlsx")

#set date as index
df.set_index('date', inplace=True)

#set frequency as quarter
df= df.resample('Q').asfreq()

#View index
df.index
Ensure the frequency is set to Quarter or Monthly
Ensure the frequency is set to Quarter or Monthly
# display data
df.head()
Revenue is in Million USD
Revenue is in Million USD

These are the only transformations you need.

Instantiate x_13_arima_analysis

# Run X-13ARIMA-SEATS decomposition
results = x13_arima_analysis(df['revenue'])

x_13_arima_analsyis combines ARIMA modeling and SEATS filtering to decompose time series data. The ARIMA (AutoRegressive Integrated Moving Average) part of the analysis models the data based on its past values and errors. The SEATS (Signal Extraction in ARIMA Time Series) part of the model focuses on isolating the time series component (trend, cycle, seasonality, irregular)

x_13_arima provides a seasonally adjusted revenue by removing the seasonality component from the actual revenue.

# Get the seasonally adjusted series
seasonally_adjusted = results.seasadj

# Visualize revenue and seasonally adjusted revenue
plt.figure(figsize=(10, 6))
plt.subplot(311)
plt.plot(df.index, df['revenue'], label='Original Data')
plt.legend()

plt.subplot(312)
plt.plot(df.index, seasonally_adjusted, label='Seasonally Adjusted')
plt.legend()

plt.tight_layout()
plt.show()
Seasonal adjustments remove the seasonal patterns (image by author)
Seasonal adjustments remove the seasonal patterns (image by author)

Validation

In this analysis, we are not focusing on forecasting capabilities. Rather, we want to analyze the seasonally adjusted data to stay close to our business trends.

However, we still need to check if the model did a good job of decomposing the time series.

You can use the QS statistic to assess the robustness of the analysis. You are aiming at a QS statistic lower than 1. The closer your QS Statistic result is to 0, the more it indicates that residuals are indistinguishable from white noise or are uncorrelated.

print(results.results)
The results print a lot of information. You will need to scroll until you reach the QS statistic.
The results print a lot of information. You will need to scroll until you reach the QS statistic.

Interpreting the results

Now that we have seasonally adjusted data, we can perform our business trend analysis in two ways.

First, we can focus on the Quarter-over-Quarter growth of the seasonally adjusted revenue.

# Calculate the % chg to the previous quarter
df['QoQ %chg'] = df['revenue'].pct_change() * 100

df['QoQ% chg adjusted'] = df['seasadj'].pct_change() * 100

# Getting the index positions for x-axis locations
x = range(len(df.index))

# Plotting the bar chart
plt.figure(figsize=(10, 6))

bar_width = 0.40

plt.bar(x, df['QoQ %chg'], width=bar_width, align='center', label='% chg', color='blue', alpha=0.7)
plt.bar([i + bar_width for i in x], df[f'QoQ% chg adjusted'], width=bar_width, align='center', label='% chg adjusted', color='teal', alpha=0.7)

# Enhance the visualization
plt.axhline(y=0, color='gray', linestyle='--', linewidth=1)
plt.xlabel('Date')
plt.ylabel('% Change')
plt.title(f'Comparison of {metric_name} % chg and % chg adjusted')
plt.legend()
plt.xticks([i + bar_width/2 for i in x], df.index.strftime('%Y-%m-%d'), rotation=45)
plt.tight_layout()

plt.show()
QoQ change (image by author)
QoQ change (image by author)

Q1 2023 QoQ % chg highlights the importance of using seasonally adjusted data. Without adjustment, the revenue shows a decline vs the previous quarter; however, once we adjust for seasonality patterns, we see a growth in revenue, indicating a strong business trend.

The opposite is true in Q3; the seasonally adjusted revenue dropped more than expected, and this should trigger further analysis.

The second analysis involves calculating the Seasonally Adjusted Annualized Rate (SAAR)

SAAR = ((seasonally adjusted revenue * 4 )/ last year revenue)-1

In the case of quarterly data, we multiply by 4 to annualize the data; if we had monthly data, we would multiply by 12. This measure helps you provide a smoothed-out, standardized view of data over the entire year.

Keep in mind that SAAR is NOT a forecast. But it can help you make informed business decisions by providing a clearer financial picture.

Knowing these two paths, we can define a function to automate your analysis.

def results_analysis(result = results, analysis_date= '2023-09-30', freq ='Quarter'):
    """
    This function takes the results from X13 arima analysis and returns a dataframe with:

        - Revenue
        - Seasonally adjusted Revenue
        - Revenue vs previous period
        - Seasonally adjusted Revenue vs previous period
        - SAAR : Seasonally adjusted annual rate
        - SAAR %chg vs last year

    The funtion also print key financial output
        - Revenue
        - Revenue vs last year
        - Revenue vs previous period
        - Seasonally adjusted Revenue vs previous period
        - SAAR
        - SAAR %chg vs last year

    Parameters
    ----------
        result : statsmodels.tsa.x13.X13ArimaAnalysisResult object
            the result from instantiating x13_arima_analysis
        analysis_date : str
            the date for analysis
        freq : str, optional
            the frequency of our data, either "Quarter" or "Month" (default is Quarter)    
    """

    #get the observed & Seasonally adjusted data into Dataframe
    observed = pd.DataFrame(result.observed)

    seasonal_adj = pd.DataFrame(result.seasadj)
    df = pd.concat([observed,seasonal_adj],axis=1)

    # get  data from previous Year until analysis_date
    analysis_date = datetime.strptime(analysis_date, '%Y-%m-%d') # convert variable to datetime

    last_year = analysis_date.year -1
    df = df[df.index.year >= last_year].copy()

    #Calculate QoQ or MoM revenue change and Sesonally adjusted revenue change
    metric_name = 'QoQ' if freq == 'Quarter' else 'MoM'

    df[f'{metric_name} %chg'] = df['revenue'].pct_change() * 100

    df[f'{metric_name}% chg adjusted'] = df['seasadj'].pct_change() * 100

    #calculate LY revenue

    ly_revenue = df[df.index.year == last_year]['revenue'].sum()

    #Calculate Seasonally Adjusted Annual Rate and chg

    annual_factor = 4 if freq == 'Quarter' else 12 # assing annual factor for SAAR calculation

    df['SAAR'] = df.apply(lambda row: row['seasadj'] * annual_factor if row.name.year == analysis_date.year else None, axis=1)

    df['SAAR % Chg'] = df.apply(lambda row: (row['SAAR'] / ly_revenue - 1)*100 if row.name.year == analysis_date.year else None, axis=1)

    data = df[df.index==analysis_date]# get the data for the analysis date
    ly_data = df[df.index==(analysis_date - relativedelta(years=1))]# get the data for the previous year analysis date

    #Print results
    print(f'{freq} Revenue: {data["revenue"][0]}')
    print(f'{freq} Revenue YoY %chg: {(data["revenue"][0]/ly_data["revenue"][0]-1)*100 :.1f}')
    print(f'{freq} Revenue {metric_name} %chg: {data[f"{metric_name} %chg"][0] :.1f}')
    print(f'{freq} Seasonally adjusted Revenue {metric_name} %chg: {data[f"{metric_name}% chg adjusted"][0] :.1f}')
    print(f'Seasonally adjusted annual rate: {data["SAAR"][0]}')
    print(f'Seasonally adjusted annual rate %chg: {data["SAAR % Chg"][0] :.1f}')

    return df

df_results = results_analysis(results)
This function can help you put all the relevant comparisons next to each other (image by author)
This function can help you put all the relevant comparisons next to each other (image by author)

These steps provide an easy framework for elevating your business analytics by catching and addressing business trend changes early on.

References

[1] U.S. Census Bureau. X-13ARIMA-SEATS Documentation. Retrieved from https://www2.census.gov/software/x-13arima-seats/x-13-data/documentation/docx13as.pdf

[2] Statsmodels. X-13 ARIMA Analysis Documentation. Retrieved from https://www.statsmodels.org/dev/_modules/statsmodels/tsa/x13.html#x13_arima_analysis

[3] Singstat. Seasonal Adjustment. Retrieved from https://www.singstat.gov.sg/find-data/quizzes/seasonal-adjustment

[4] Macrotrends. Tesla Income Statement. Retrieved from https://www.macrotrends.net/stocks/charts/TSLA/tesla/income-statement?freq=Q

[5] JDemetra+ Documentation. Seasonally Adjusted Output – X13. Retrieved from https://jdemetradocumentation.github.io/JDemetra-documentation/pages/reference-manual/sa-output-X13.html

[6] Conerly, Bill (2014, December 17). How to Adjust Your Business Data for Seasonality. Forbes. Retrieved from https://www.forbes.com/sites/billconerly/2014/12/17/how-to-adjust-your-business-data-for-seasonality/?sh=3b3522ed421c

[7] Investopedia. Seasonal Adjustment. Retrieved from https://www.investopedia.com/terms/s/seasonal-adjustment.asp

[8] Federal Reserve Bank of Dallas. Seasonally Adjusted Data. Retrieved from https://www.dallasfed.org/research/basics/seasonally


Related Articles