you for the sort response to Part 1, it’s been encouraging to see so many readers all for time series forecasting.
In Part 1 of this series, we broke down time series data into trend, seasonality, and noise, discussed when to make use of additive versus multiplicative models, and built a Seasonal Naive baseline forecast using Each day Temperature Data. We evaluated its performance using MAPE (Mean Absolute Percentage Error), which got here out to twenty-eight.23%.
While the Seasonal Naive model captured the broad seasonal pattern, we also saw that it will not be the very best fit for this dataset, because it doesn’t account for subtle shifts in seasonality or long-term trends. This highlights the necessity to transcend basic baselines and customize forecasting models to raised reflect the underlying data for improved accuracy.
After we applied the Seasonal Naive baseline model, we didn’t account for the trend or use any mathematical formulas, we simply predicted each value based on the identical day from the previous 12 months.
First, let’s take a have a look at the table below, which outlines some common baseline models and when to make use of every one.
These are a number of the mostly used baseline models across various industries.
But what if the information shows each trend and seasonality? In such cases, these easy baseline models may not be enough. As we saw in Part 1, the Seasonal Naive model struggled to completely capture the patterns in the information, leading to a MAPE of 28.23%.
So, should we jump straight to ARIMA or one other complex forecasting model?
Not necessarily.
Before reaching for advanced tools, we will first construct our baseline model based on the structure of the information. This helps us construct a stronger benchmark — and infrequently, it’s enough to choose whether a more sophisticated model is even needed.
Now that we have now examined the structure of the information, which clearly includes each trend and seasonality, we will construct a baseline model that takes each components into consideration.
In Part 1, we used the seasonal decompose method in Python to visualise the trend and seasonality in our data. Now, we’ll take this a step further by actually extracting the trend and seasonal components from that decomposition and using them to construct a baseline forecast.

But before we start, let’s see how the seasonal decompose method figures out the trend and seasonality in our data.
Before using the built-in function, let’s take a small sample from our temperature data and manually undergo how the seasonal_decompose method separates trend, seasonality and residuals.
This can help us understand what’s really happening behind the scenes.

Here, we consider a 14-day sample from the temperature dataset to raised understand how decomposition works step-by-step.
We already know that this dataset follows an additive structure, which implies each observed value is made up of three parts:
Observed Value = Trend + Seasonality + Residual.
First, let’s have a look at how the trend is calculated for this sample.
We’ll use a 3-day centered moving average, which implies each value is averaged with its immediate neighbor on each side. This helps smooth out day-to-day variations in the information.
For instance, to calculate the trend for February 1, 1981:
Trend = (20.7 + 17.9 + 18.8) / 3
= 19.13
This manner, we calculate the trend component for all 14 days within the sample.

Here’s the table showing the 3-day centered moving average trend values for every day in our 14-day sample.
As we will see, the trend values for the primary and last dates are ‘NaN’ because there aren’t enough neighboring values to calculate a centered average at those points.
We’ll revisit those missing values once we finish computing the seasonality and residual components.
Before we dive into seasonality, there’s something we said earlier that we should always come back to. We mentioned that using a 3-day centered moving average helps in smoothing out each day variations in the information — but what does that actually mean?
Let’s have a look at a fast example to make it clearer.
We’ve already discussed that the trend reflects the general direction the information is moving in.
Temperatures are generally higher in summer and lower in winter, that’s the broad seasonal pattern we expect.
But even inside summer, temperatures don’t stay the exact same day by day. Some days may be barely cooler or warmer than others. These are natural every day fluctuations, not signs of sudden climate shifts.
The moving average helps us smooth out these short-term ups and downs so we will deal with the larger picture, the underlying trend across time.
Since we’re working with a small sample here, the trend may not stand out clearly just yet.
But when you have a look at the total decomposition plot above, you may see how the trend captures the general direction the information is moving in, step by step rising, falling or staying regular over time.
Now that we’ve calculated the trend, it’s time to maneuver on to the subsequent component: seasonality.
We all know that in an additive model:
Observed Value = Trend + Seasonality + Residual
To isolate seasonality, we start by subtracting the trend from the observed values:
Observed Value – Trend = Seasonality + Residual
The result’s often called the detrended series — a mix of the seasonal pattern and any remaining random noise.
Let’s take January 2, 1981 for instance.
Observed temperature: 17.9°C
Trend: 19.13°C
So, the detrended value is:
Detrended = 17.9 – 19.1 = -1.23
In the identical way, we calculate the detrended values for all of the dates in our sample.

The table above shows the detrended values for every date in our 14-day sample.
Since we’re working with 14 consecutive days, we’ll assume a weekly seasonality and assign a Day Index (from 1 to 7) to every date based on its position in that 7-day cycle.

Now, to estimate seasonality, we take the common of the detrended values that share the identical Day Index.
Let’s calculate the seasonality for January 2, 1981. The Day Index for this date is 2, and the opposite date in our sample with the identical index is January 9, 1981. To estimate the seasonal effect for this index, we take the common of the detrended values from each days. This seasonal effect will then be assigned to each date with Index 2 in our cycle.
for January 2, 1981: Detrended value = -1.2 and
for January 9, 1981: Detrended value = 2.1
Average of each values = (-1.2 + 2.1)/2
= 0.45
So, 0.45 is the estimated seasonality for all dates with Index 2.
We repeat this process for every index to calculate the total set of seasonality components.

Listed here are the values of seasonality for all of the dates and these seasonal values reflect the recurring pattern across the week. For instance, days with Index 2 are inclined to be around 0.45oC warmer than the trend on average, while days with Index 4 are inclined to be 1.05oC cooler.
Note: After we say that days with Index 2 are inclined to be around +0.45°C warmer than the trend on average, we mean that dates like Jan 2 and Jan 9 are inclined to be about 0.45°C above their very own trend value, not in comparison with the general dataset trend, but to the local trend specific to every day.
Now that we’ve calculated the seasonal components for every day, you may notice something interesting: even the dates where the trend (and subsequently detrended value) was missing, just like the first and last dates in our sample — still received a seasonality value.
It is because seasonality is assigned based on the Day Index, which follows a repeating cycle (like 1 to 7 in our weekly example).
So, if January 1 has a missing trend but shares the identical index as, say, January 8, it inherits the identical seasonal effect that was calculated using valid data from that index group.
In other words, seasonality doesn’t depend upon the supply of trend for that specific day, but slightly on the pattern observed across all days with the identical position within the cycle.
Now we calculate the residual, based on the additive decomposition structure we all know that:
Observed Value = Trend + Seasonality + Residual
…which implies:
Residual = Observed Value – Trend – Seasonality
You may be wondering, if the detrended values we used to calculate seasonality already had residuals in them, how can we separate them now? The reply comes from averaging. After we group the detrended values by their seasonal position, like Day Index, the random noise tends to cancel itself out. What we’re left with is the repeating seasonal signal. In small datasets this may not be very noticeable, but in larger datasets, the effect is rather more clear. And now, with each trend and seasonality removed, what stays is the residual.

We will observe that residuals aren’t calculated for the primary and last dates, for the reason that trend wasn’t available there resulting from the centered moving average.
Let’s take a have a look at the ultimate decomposition table for our 14-day sample. This brings together the observed temperatures, the extracted trend and seasonality components, and the resulting residuals.

Now that we’ve calculated the trend, seasonality, and residuals for our sample, let’s come back to the missing values we mentioned earlier. If you happen to have a look at the decomposition plot for the total dataset, titled “Decomposition of every day temperatures showing trend, seasonal cycles, and random fluctuations”, you’ll notice that the trend line doesn’t appear right at first of the series. The identical applies to residuals. This happens because calculating the trend requires enough data before and after each point, so the primary few and previous couple of values don’t have an outlined trend. That’s also why we see missing residuals at the perimeters. But in large datasets, these missing values make up only a small portion and don’t affect the general interpretation. You may still clearly see the trend and patterns over time. In our small 14-day sample, these gaps feel more noticeable, but in real-world time series data, this is totally normal and expected.
Now that we’ve understood how seasonal_decompose works, let’s take a fast have a look at the code we used to use it to the temperature data and extract the trend and seasonality components.
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
# Load the dataset
df = pd.read_csv("minimum every day temperatures data.csv")
# Convert 'Date' to datetime and set as index
df['Date'] = pd.to_datetime(df['Date'], dayfirst=True)
df.set_index('Date', inplace=True)
# Set a daily every day frequency and fill missing values using forward fill
df = df.asfreq('D')
df['Temp'].fillna(method='ffill', inplace=True)
# Decompose the every day series (365-day seasonality for yearly patterns)
decomposition = seasonal_decompose(df['Temp'], model='additive', period=365)
# Plot the decomposed components
decomposition.plot()
plt.suptitle('Decomposition of Each day Minimum Temperatures (Each day)', fontsize=14)
plt.tight_layout()
plt.show()
Let’s deal with this a part of the code:
decomposition = seasonal_decompose(df['Temp'], model='additive', period=365)
On this line, we’re telling the function what data to make use of (df['Temp']
), which model to use (additive
), and the seasonal period to think about (365
), which matches the yearly cycle in our every day temperature data.
Here, we set period=365
based on the structure of the information. This implies the trend is calculated using a 365-day centered moving average, which takes 182 values before and after each point. The seasonality is calculated using a 365-day seasonal index, where all January 1st values across years are grouped and averaged, all January 2nd values are grouped, and so forth.
When using seasonal_decompose
in Python, we simply provide the period
, and the function uses that value to find out how each the trend and seasonality ought to be calculated.
In our earlier 14-day sample, we used a 3-day centered average simply to make the mathematics more comprehensible — however the underlying logic stays the identical.
Now that we’ve explored how seasonal_decompose
works and understood the way it separates a time series into trend, seasonality, and residuals, we’re able to construct a baseline forecasting model.
This model might be constructed by simply adding the extracted trend and seasonality components, essentially assuming that the residual (or noise) is zero.
Once we generate these baseline forecasts, we’ll evaluate how well they perform by comparing them to the actual observed values using MAPE (Mean Absolute Percentage Error).
Here, we’re ignoring the residuals because we’re constructing a straightforward baseline model that serves as a benchmark. The goal is to check whether more advanced algorithms are truly needed.
We’re primarily all for seeing how much of the variation in the information might be explained using just the trend and seasonality components.
Now we’ll construct a baseline forecast by extracting the trend and seasonality components using Python’s seasonal_decompose
.
Code:
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
from sklearn.metrics import mean_absolute_percentage_error
# Load the dataset
df = pd.read_csv("/minimum every day temperatures data.csv")
# Convert 'Date' to datetime and set as index
df['Date'] = pd.to_datetime(df['Date'], dayfirst=True)
df.set_index('Date', inplace=True)
# Set a daily every day frequency and fill missing values using forward fill
df = df.asfreq('D')
df['Temp'].fillna(method='ffill', inplace=True)
# Split into training (all years except final) and testing (final 12 months)
train = df[df.index.12 months < df.index.year.max()]
test = df[df.index.year == df.index.year.max()]
# Decompose training data only
decomposition = seasonal_decompose(train['Temp'], model='additive', period=365)
# Extract components
trend = decomposition.trend
seasonal = decomposition.seasonal
# Use last full year of seasonal values from training to repeat for test
seasonal_values = seasonal[-365:].values
seasonal_test = pd.Series(seasonal_values[:len(test)], index=test.index)
# Extend last valid trend value as constant across the test period
trend_last = trend.dropna().iloc[-1]
trend_test = pd.Series(trend_last, index=test.index)
# Create baseline forecast
baseline_forecast = trend_test + seasonal_test
# Evaluate using MAPE
actual = test['Temp']
mask = actual > 1e-3 # avoid division errors on near-zero values
mape = mean_absolute_percentage_error(actual[mask], baseline_forecast[mask])
print(f"MAPE for Baseline Model on Final 12 months: {mape:.2%}")
# Plot actual vs. forecast
plt.figure(figsize=(12, 5))
plt.plot(actual.index, actual, label='Actual', linewidth=2)
plt.plot(actual.index, baseline_forecast, label='Baseline Forecast', linestyle='--')
plt.title('Baseline Forecast vs. Actual (Final 12 months)')
plt.xlabel('Date')
plt.ylabel('Temperature (°C)')
plt.legend()
plt.tight_layout()
plt.show()
MAPE for Baseline Model on Final 12 months: 21.21%

Within the code above, we first split the information through the use of the primary 9 years because the training set and the ultimate 12 months because the test set.
We then applied seasonal_decompose
to the training data to extract the trend and seasonality components.
For the reason that seasonal pattern repeats yearly, we took the last 365 seasonal values and applied them to the test period.
For the trend, we assumed it stays constant and used the last observed trend value from the training set across all dates within the test 12 months.
Finally, we added the trend and seasonality components to construct the baseline forecast, compared it with the actual values from the test set, and evaluated the model using Mean Absolute Percentage Error (MAPE).
We got a MAPE of 21.21% with our baseline model. In Part 1, the seasonal naive approach gave us 28.23%, so we’ve improved by about 7%.
What we’ve built here is just not a custom baseline model — it’s a standard decomposition-based baseline.
Let’s now see how we will provide you with our own custom baseline for this temperature data.
Now let’s consider the common of temperatures grouped by every day and using them forecast the temperatures for final 12 months.
You may be wondering how we even provide you with that concept for a custom baseline in the primary place. Truthfully, it starts by simply taking a look at the information. If we will spot a pattern, like a seasonal trend or something that repeats over time, we will construct a straightforward rule around it.
That’s really what a custom baseline is about — using what we understand from the information to make an affordable prediction. And infrequently, even small, intuitive ideas can work surprisingly well.
Now let’s use Python to calculate the common temperature for every day of the 12 months.
Code:
# Create a brand new column 'day_of_year' representing which day (1 to 365) each date falls on
train["day_of_year"] = train.index.dayofyear
test["day_of_year"] = test.index.dayofyear
# Group the training data by 'day_of_year' and calculate the mean temperature for every day (averaged across all years)
daily_avg = train.groupby("day_of_year")["Temp"].mean()
# Use the learned seasonal pattern to forecast test data by mapping test days to the corresponding every day average
day_avg_forecast = test["day_of_year"].map(daily_avg)
# Evaluate the performance of this seasonal baseline forecast using Mean Absolute Percentage Error (MAPE)
mape_day_avg = mean_absolute_percentage_error(test["Temp"], day_avg_forecast)
round(mape_day_avg * 100, 2)
To construct this practice baseline, we checked out how the temperature typically behaves on every day of the 12 months, averaging across all of the training years. Then, we used those every day averages to make predictions for the test set. It’s a straightforward strategy to capture the seasonal pattern that tends to repeat yearly.
This tradition baseline gave us a MAPE of 21.17%, which shows how well it captures the seasonal trend in the information.
Now, let’s see if we will construct one other custom baseline that captures patterns in the information more effectively and serves as a stronger benchmark.
Now that we’ve used the day-of-year average method for our first custom baseline, you may start wondering what happens in leap years. If we simply number the times from 1 to 365 and take the common, we could find yourself misled, especially around February 29.
You may be wondering if a single date really matters. In time series evaluation, every moment counts. It could not feel that vital straight away since we’re working with a straightforward dataset, but in real-world situations, small details like this could have a big effect. Many industries pay close attention to those patterns, and even a one-day difference can affect decisions. That’s why we’re starting with a straightforward dataset, to assist us understand these ideas clearly before applying them to more complex problems.
Now let’s construct a custom baseline using calendar-day averages by taking a look at how the temperature often behaves on each (month, day) across years.
It’s a straightforward strategy to capture the seasonal rhythm of the 12 months based on the actual calendar.
Code:
# Extract the 'month' and 'day' from the datetime index in each training and test sets
train["month"] = train.index.month
train["day"] = train.index.day
test["month"] = test.index.month
test["day"] = test.index.day
# Group the training data by each (month, day) pair and calculate the common temperature for every calendar day
calendar_day_avg = train.groupby(["month", "day"])["Temp"].mean()
# Forecast test values by mapping each test row's (month, day) to the common from training data
calendar_day_forecast = test.apply(
lambda row: calendar_day_avg.get((row["month"], row["day"]), np.nan), axis=1
)
# Evaluate the forecast using Mean Absolute Percentage Error (MAPE)
mape_calendar_day = mean_absolute_percentage_error(test["Temp"], calendar_day_forecast)
Using this method, we achieved a MAPE of 21.09%.
Now let’s see if we will mix two methods to construct a more refined custom baseline. Now we have already created a calendar-based month-day average baseline. This time we’ll mix it with the day prior to this’s actual temperature. The forecasted value might be based 70 percent on the calendar day average and 30 percent on the day prior to this’s temperature, making a more balanced and adaptive prediction.
# Create a column with the day prior to this's temperature
df["Prev_Temp"] = df["Temp"].shift(1)
# Add the day prior to this's temperature to the test set
test["Prev_Temp"] = df.loc[test.index, "Prev_Temp"]
# Create a blended forecast by combining calendar-day average and former day's temperature
# 70% weight to seasonal calendar-day average, 30% to previous day temperature
blended_forecast = 0.7 * calendar_day_forecast.values + 0.3 * test["Prev_Temp"].values
# Handle missing values by replacing NaNs with the common of calendar-day forecasts
blended_forecast = np.nan_to_num(blended_forecast, nan=np.nanmean(calendar_day_forecast))
# Evaluate the forecast using MAPE
mape_blended = mean_absolute_percentage_error(test["Temp"], blended_forecast)
We will call this a blended custom baseline model. Using this approach, we achieved a MAPE of 18.73%.
Let’s take a moment to summarize what we’ve applied to this dataset to this point using a straightforward table.

In Part 1, we used the seasonal naive method as our baseline. On this blog, we explored how the seasonal_decompose
function in Python works and built a baseline model by extracting its trend and seasonality components. We then created our first custom baseline using a straightforward idea based on the day of the 12 months and later improved it through the use of calendar day averages. Finally, we built a blended custom baseline by combining the calendar average with the day prior to this’s temperature, which led to even higher forecasting results.
On this blog, we used a straightforward every day temperature dataset to know how custom baseline models work. Because it’s a univariate dataset, it accommodates only a time column and a goal variable. Nevertheless, real-world time series data is commonly rather more complex and typically multivariate, with multiple influencing aspects. Before we explore the way to construct custom baselines for such complex datasets, we’d like to know one other vital decomposition method called STL decomposition. We also need a solid grasp of univariate forecasting models like ARIMA and SARIMA. These models are essential because they form the inspiration for understanding and constructing more advanced multivariate time series models.
In Part 1, I discussed that we might explore the foundations of ARIMA on this part as well. Nevertheless, as I’m also learning and desired to keep things focused and digestible, I wasn’t capable of fit every thing into one blog. To make the educational process smoother, we’ll take it one topic at a time.
In Part 3, we’ll explore STL decomposition and proceed constructing on what we’ve learned to this point.
Dataset and License
The dataset utilized in this text — — is obtainable on Kaggle and is shared under the Community Data License Agreement – Permissive, Version 1.0 (CDLA-Permissive 1.0).
That is an open license that allows industrial use with proper attribution. You may read the total license here.
I hope you found this part helpful and simple to follow.
Thanks for reading and see you in Part 3!