Mathew K Analytics

Lesson 50 · Finance and Stock Market Analytics

Seasonal Patterns in Financial Markets: Understanding Recurring Trends

In this lesson, we will learn how to detect and analyze seasonal trends in stock prices using Python. Seasonal trends, such as the 'January effect' or…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Seasonal Patterns in Financial Markets#

  • In this lesson, we will learn how to detect and analyze seasonal trends in stock prices using Python.
  • Seasonal trends, such as the 'January effect' or holiday rallies, can impact trading strategies and investment decisions.
  • We will work with real-world historical data and build practical skills to quantify, visualize, and reason about seasonality in the stock market.
  • By the end, you will understand if and how certain stocks show repeating trends at certain times of year.
  • This is a vital skill for finance professionals, traders, and anyone who works with market data.
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf

Core Data Concepts for Seasonality Analysis#

  • We will use real daily stock price data for multiple companies.
  • The 'ohlcv_multi' dataset contains one row per day and per ticker.
  • For seasonality, we care about (1) the date, (2) the ticker, and (3) the closing price.
  • Beginners often confuse multi-indexed columns with normal columns in yfinance outputs.
  • It is important to use clean, long-format data to avoid indexing mistakes and misaligned statistics.
tickers = ['AAPL','MSFT','GOOGL','AMZN','TSLA']
ohlcv = yf.download(tickers, period='1y', auto_adjust=True, progress=False)
ohlcv = ohlcv.stack(future_stack=True).rename_axis(['Date','Ticker']).reset_index()
ohlcv.columns.name = None
print(ohlcv.shape)
print(ohlcv.head(3))
(1255, 7)
        Date Ticker       Close        High         Low        Open    Volume
0 2025-07-15   AAPL  208.283691  211.052705  208.094440  208.393257  42296300
1 2025-07-15   AMZN  226.350006  227.270004  225.460007  226.199997  34907300
2 2025-07-15  GOOGL  181.482269  183.695955  181.083413  182.289963  33448300
# Beginner Example 1: How does AAPL price change by month?
aapl = ohlcv[ohlcv['Ticker'] == 'AAPL'].copy()
aapl['Month'] = pd.to_datetime(aapl['Date']).dt.month
monthly_means = aapl.groupby('Month')['Close'].mean().round(2)
print(monthly_means)
Month
1     257.17
2     268.86
3     254.67
4     264.45
5     297.46
6     296.42
7     251.98
8     223.94
9     241.80
10    257.57
11    271.08
12    275.79
Name: Close, dtype: float64
# Beginner Example 2: Plotting monthly average price for Apple
monthly_means.plot(kind='bar', title='AAPL Average Closing Price by Month')
plt.ylabel('Average Closing Price (USD)')
plt.xlabel('Month')
plt.show()
No description has been provided for this image
# Beginner Example 3: Add month names for clarity
month_names = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
               7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'}
monthly_means.index = monthly_means.index.map(month_names)
monthly_means.plot(kind='bar', color='skyblue', title='AAPL Average Price by Calendar Month')
plt.ylabel('Average Closing Price (USD)')
plt.show()
No description has been provided for this image
# Intermediate Example 1: Compute average monthly returns instead of price
aapl['Return'] = aapl['Close'].pct_change()
aapl['Month'] = pd.to_datetime(aapl['Date']).dt.month
monthly_returns = aapl.groupby('Month')['Return'].mean().mul(100).round(3)
monthly_returns.index = monthly_returns.index.map(month_names)
print(monthly_returns)
Month
Jan   -0.225
Feb    0.122
Mar   -0.175
Apr    0.330
May    0.712
Jun   -0.336
Jul    0.375
Aug    0.555
Sep    0.457
Oct    0.271
Nov    0.172
Dec   -0.113
Name: Return, dtype: float64
# Intermediate Example 2: Visualize mean monthly returns for AAPL
monthly_returns.plot(kind='bar', color='orange', title='AAPL Mean Monthly Returns (%)')
plt.ylabel('Mean Monthly Return (%)')
plt.xlabel('Month')
plt.axhline(0, color='gray', linestyle='--')
plt.show()
No description has been provided for this image
# Intermediate Example 3: Compare monthly returns of all five stocks
monthly_returns_all = (ohlcv.assign(Month = pd.to_datetime(ohlcv['Date']).dt.month)
    .groupby(['Ticker','Month'])['Close'].apply(lambda x: x.pct_change().mean()*100).unstack(0).round(2))
monthly_returns_all.index = monthly_returns_all.index.map(month_names)
print(monthly_returns_all)
Ticker  AAPL  AMZN  GOOGL  MSFT  TSLA
Month                                
Jan    -0.22  0.30   0.38 -0.46 -0.06
Feb    -0.10 -0.78  -0.53 -0.38 -0.24
Mar    -0.19  0.02  -0.28 -0.34 -0.36
Apr     0.31  1.17   1.33  0.52  0.04
May     0.58  0.06  -0.06  0.46  0.60
Jun    -0.26 -0.43  -0.23 -1.02  0.12
Jul     2.31  0.43   4.46 -1.04  1.50
Aug     0.71  0.33   0.60 -0.16  0.52
Sep     0.53 -0.12   0.73  0.13  1.55
Oct     0.27  0.50   0.64 -0.01  0.02
Nov     0.21 -0.45   0.70 -0.26 -0.42
Dec    -0.19 -0.06  -0.02 -0.02  0.24
# Intermediate Example 4: Visualize all stocks' monthly returns in one chart
monthly_returns_all.plot(kind='bar', figsize=(12,5), title='Mean Monthly Returns by Ticker (%)')
plt.ylabel('Mean Monthly Return (%)')
plt.xlabel('Month')
plt.axhline(0, color='gray', linestyle='--', linewidth=1)
plt.legend(title='Ticker')
plt.tight_layout()
plt.show()
No description has been provided for this image
# Intermediate Example 5: Which month has the highest average return overall?
monthly_returns_all['Average'] = monthly_returns_all.mean(axis=1)
best_month = monthly_returns_all['Average'].idxmax()
print(f'Highest average return month: {best_month}')
Highest average return month: Jul
# Advanced Example 1: Analyzing holiday season effects (Nov-Dec)
holiday_months = ['Nov', 'Dec']
holiday_returns = monthly_returns_all.loc[holiday_months]
print('Mean returns for Nov/Dec by ticker:')
print(holiday_returns)
Mean returns for Nov/Dec by ticker:
Ticker  AAPL  AMZN  GOOGL  MSFT  TSLA  Average
Month                                         
Nov     0.21 -0.45   0.70 -0.26 -0.42   -0.044
Dec    -0.19 -0.06  -0.02 -0.02  0.24   -0.010
# Advanced Example 2: Test statistical significance of seasonality
from scipy.stats import f_oneway
returns_by_month = [aapl[aapl['Month']==m]['Return'].dropna() for m in range(1,13)]
fstat, pval = f_oneway(*returns_by_month)
print(f'ANOVA F-stat: {fstat:.4f}, p-value: {pval:.4f}')
if pval < 0.05:
    print('Significant monthly return differences detected!')
else:
    print('No significant difference in monthly returns.')
ANOVA F-stat: 0.9938, p-value: 0.4527
No significant difference in monthly returns.
# Advanced Example 3: Heatmap for visualizing returns by ticker and month
import seaborn as sns
plt.figure(figsize=(9,5))
sns.heatmap(monthly_returns_all.T, annot=True, cmap='RdYlGn', center=0)
plt.title('Monthly Mean Returns Heatmap (%)')
plt.xlabel('Month')
plt.ylabel('Ticker')
plt.show()
No description has been provided for this image
# Advanced Example 4: Rolling 3-month seasonal returns
def three_month_rolling(df):
    df = df.sort_values('Date')
    df['3mo_ret'] = df['Close'].pct_change(periods=63)
    df['Month'] = pd.to_datetime(df['Date']).dt.month
    return df

aapl_3mo = three_month_rolling(aapl)
three_monthly = aapl_3mo.groupby('Month')['3mo_ret'].mean().mul(100).round(2)
three_monthly.index = three_monthly.index.map(month_names)
print(three_monthly)
Month
Jan     0.37
Feb    -0.51
Mar    -7.83
Apr     2.44
May    10.91
Jun    16.37
Jul    21.11
Aug      NaN
Sep      NaN
Oct    23.74
Nov    19.93
Dec    14.01
Name: 3mo_ret, dtype: float64
# Error Handling Example 1: What if data for a month is missing?
missing_months = set(range(1,13)) - set(aapl['Month'])
print(f'Missing data for months: {missing_months}')
Missing data for months: set()
# Error Handling Example 2: Handling zeros or NaNs in returns
bad_rows = aapl[aapl['Return'].isna()]
print('Rows with missing return:', bad_rows)
Rows with missing return:         Date Ticker       Close        High        Low        Open    Volume  \
0 2025-07-15   AAPL  208.283691  211.052705  208.09444  208.393257  42296300   

   Month  Return  
0      7     NaN  
# Best Practice 1: Encapsulate seasonality logic as a reusable function
def get_monthly_returns(df, ticker):
    sub = df[df['Ticker']==ticker].copy()
    sub['Return'] = sub['Close'].pct_change()
    sub['Month'] = pd.to_datetime(sub['Date']).dt.month
    return sub.groupby('Month')['Return'].mean().mul(100).round(2)

msft_monthly_returns = get_monthly_returns(ohlcv, 'MSFT')
print(msft_monthly_returns)
Month
1    -0.55
2    -0.45
3    -0.26
4     0.48
5     0.52
6    -0.86
7     0.41
8    -0.23
9     0.11
10    0.01
11   -0.25
12   -0.07
Name: Return, dtype: float64
# Best Practice 2: Visualize uncertainty using error bars
means = aapl.groupby('Month')['Return'].mean().mul(100)
stds = aapl.groupby('Month')['Return'].std().mul(100)
plt.bar(month_names.values(), means, yerr=stds, capsize=6, alpha=0.6, color='slateblue')
plt.title('AAPL Mean Monthly Return with Standard Deviation')
plt.ylabel('Monthly Return (%)')
plt.xlabel('Month')
plt.axhline(0, color='gray', linestyle='dashed')
plt.show()
No description has been provided for this image
# End-to-End Example: Identify the best and worst months for TSLA over the last year
def annotate_extremes(monthly_ret):
    best = monthly_ret.idxmax()
    worst = monthly_ret.idxmin()
    print(f'Best month for TSLA: {month_names[best]} ({monthly_ret[best]:.2f}%)')
    print(f'Worst month for TSLA: {month_names[worst]} ({monthly_ret[worst]:.2f}%)')

tsla_ret = get_monthly_returns(ohlcv, 'TSLA')
annotate_extremes(tsla_ret)
Best month for TSLA: Sep (1.41%)
Worst month for TSLA: Feb (-0.33%)

Explore More: Deep-Dive and Next Steps#

  • Seasonality is only one component of price movementsalways combine with fundamental analysis before investing.
  • Try these ideas for extra practice:
    • Check intra-week seasonality (are some weekdays stronger?)
    • Study quarterly instead of monthly patterns.
    • Compare across different sectors or countries.
  • Practice analyzing sector ETFs for recurring patterns or compare 'January effect' across years.
  • Like this lesson? Search for more deep divessubscribe to our YouTube for expert-led walkthroughs!

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.