Mathew K Analytics

Lesson 48 · Python for Retail E-commerce Analytics

Seasonal Demand Forecasting Training with Python for Retail E-commerce Analytics

We will learn how to analyze sales data to uncover seasonal demand patterns in retail and e-commerce. Accurate demand forecasting helps retailers optimize…

⬇ 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 Demand Forecasting in Retail and E-Commerce#

  • We will learn how to analyze sales data to uncover seasonal demand patterns in retail and e-commerce.
  • Accurate demand forecasting helps retailers optimize inventory, plan promotions, and reduce stockouts or overstocking.
  • By the end, you will be able to use Python to detect and forecast seasonal trends, giving actionable insights for better business decisions.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

Retail Analytics Concepts for Demand Forecasting#

  • In retail, datasets usually contain sales transactions, product details, and customer orders.
  • Each sale often has an invoice, item, quantity, price, and timestamp.
  • Revenue, order size, and product-level sales are calculated using these columns.
  • Common beginner mistakes include: not converting dates to datetime, forgetting to sum quantity for total sales, and grouping by the wrong column (like ID instead of Date or Product).
  • Understanding the meaning behind each field is crucial for accurate forecasting.
# Load a real online retail dataset from UCI
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df = pd.read_excel(url, sheet_name='Year 2010-2011')
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
print(df.shape)
print(df.head(3))
(541910, 8)
  Invoice StockCode                         Description  Quantity  \
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   
1  536365     71053                 WHITE METAL LANTERN         6   
2  536365    84406B      CREAM CUPID HEARTS COAT HANGER         8   

          InvoiceDate  Price  Customer ID         Country  
0 2010-12-01 08:26:00   2.55      17850.0  United Kingdom  
1 2010-12-01 08:26:00   3.39      17850.0  United Kingdom  
2 2010-12-01 08:26:00   2.75      17850.0  United Kingdom  
# Calculate daily sales quantity
daily_qty = df.groupby(df['InvoiceDate'].dt.date)['Quantity'].sum()
print(daily_qty.head())
InvoiceDate
2010-12-01    26814
2010-12-02    21023
2010-12-03    14830
2010-12-05    16395
2010-12-06    21419
Name: Quantity, dtype: int64
# Calculate daily sales revenue
df['Revenue'] = df['Quantity'] * df['Price']
daily_revenue = df.groupby(df['InvoiceDate'].dt.date)['Revenue'].sum()
print(daily_revenue.head())
InvoiceDate
2010-12-01    58635.56
2010-12-02    46207.28
2010-12-03    45620.46
2010-12-05    31383.95
2010-12-06    53860.18
Name: Revenue, dtype: float64
# Visualize daily sales quantity over time
daily_qty.plot(figsize=(12,5), title='Daily Products Sold')
plt.ylabel('Quantity Sold')
plt.xlabel('Date')
plt.show()
No description has been provided for this image
# Extract month and weekday for seasonal analysis
df['Month'] = df['InvoiceDate'].dt.month
df['Weekday'] = df['InvoiceDate'].dt.day_name()
print(df[['InvoiceDate','Month','Weekday']].head(3))
          InvoiceDate  Month    Weekday
0 2010-12-01 08:26:00     12  Wednesday
1 2010-12-01 08:26:00     12  Wednesday
2 2010-12-01 08:26:00     12  Wednesday
# Aggregate revenue by month
monthly_revenue = df.groupby('Month')['Revenue'].sum()
print(monthly_revenue)
Month
1      560000.260
2      498062.650
3      683267.080
4      493207.121
5      723333.510
6      691123.120
7      681300.111
8      682680.510
9     1019687.622
10    1070704.670
11    1461756.250
12    1182643.030
Name: Revenue, dtype: float64
# Plot monthly sales revenue
monthly_revenue.plot(kind='bar', title='Monthly Sales Revenue', color='skyblue')
plt.ylabel('Revenue')
plt.xlabel('Month')
plt.show()
No description has been provided for this image
# Aggregate sales by weekday
weekday_sales = df.groupby('Weekday')['Revenue'].sum().reindex(['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'])
print(weekday_sales)
Weekday
Monday       1588609.431
Tuesday      1966182.791
Wednesday    1734147.010
Thursday     2112519.000
Friday       1540628.811
Saturday             NaN
Sunday        805678.891
Name: Revenue, dtype: float64
# Plot weekday sales revenue
weekday_sales.plot(kind='bar', color='salmon', title='Weekly Revenue by Day')
plt.ylabel('Revenue')
plt.xlabel('Weekday')
plt.show()
No description has been provided for this image
# Use rolling mean to smooth daily sales (reduces noise)
rolling_qty = daily_qty.rolling(window=7, min_periods=1).mean()
plt.figure(figsize=(12, 5))
plt.plot(daily_qty.index, daily_qty.values, alpha=0.4, label='Daily Sales')
plt.plot(rolling_qty.index, rolling_qty.values, color='red', label='7-Day Rolling Mean')
plt.title('Smoothed Daily Product Sales')
plt.xlabel('Date')
plt.ylabel('Quantity Sold')
plt.legend()
plt.show()
No description has been provided for this image
# Simulate more recent retail order data for time series practice
np.random.seed(42)
n_orders = 1000
order_dates = pd.date_range('2023-01-01', periods=n_orders, freq='h')
order_qty = np.random.choice([1,2,3,4], n_orders, p=[0.5,0.3,0.15,0.05])
orders_df = pd.DataFrame({'OrderDate': order_dates, 'Quantity': order_qty})
orders_df['Date'] = orders_df['OrderDate'].dt.date
daily_sim_qty = orders_df.groupby('Date')['Quantity'].sum()
print(daily_sim_qty.head())
Date
2023-01-01    40
2023-01-02    40
2023-01-03    45
2023-01-04    40
2023-01-05    42
Name: Quantity, dtype: int64
# Detect potential seasonality in simulated data
plt.figure(figsize=(12,5))
plt.plot(daily_sim_qty.index, daily_sim_qty.values, label='Simulated Daily Sales')
plt.title('Simulated Daily Retail Orders')
plt.xlabel('Date')
plt.ylabel('Quantity Sold')
plt.legend()
plt.show()
No description has been provided for this image
# Resample orders to monthly totals in the simulated dataset
orders_df['OrderDate'] = pd.to_datetime(orders_df['OrderDate'])
monthly_sim_qty = orders_df.resample('M', on='OrderDate')['Quantity'].sum()
print(monthly_sim_qty)
OrderDate
2023-01-31    1313
2023-02-28     429
Freq: ME, Name: Quantity, dtype: int64
# Plot simulated monthly demand
monthly_sim_qty.plot(kind='bar', color='orange', title='Monthly Simulated Orders')
plt.ylabel('Quantity Sold')
plt.xlabel('Month')
plt.show()
No description has been provided for this image
# Advanced: Calculate 30-day moving average on real retail sales
moving_avg_qty = daily_qty.rolling(window=30, min_periods=7).mean()
plt.figure(figsize=(12, 5))
plt.plot(moving_avg_qty.index, moving_avg_qty.values, color='navy')
plt.title('30-Day Moving Average of Daily Products Sold')
plt.ylabel('Quantity Sold')
plt.xlabel('Date')
plt.show()
No description has been provided for this image
# Advanced: Decompose timeseries to visualize seasonality
from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(daily_qty, model='additive', period=30)
result.plot()
plt.suptitle('Seasonal Decomposition of Sales: Trend, Seasonality, Residuals')
plt.show()
No description has been provided for this image
# Error Handling: Check for missing dates in sales
all_dates = pd.date_range(df['InvoiceDate'].min().date(), df['InvoiceDate'].max().date())
missing_dates = set(all_dates.date) - set(daily_qty.index)
print(f'Missing daily sales records: {len(missing_dates)}')
Missing daily sales records: 69
# Error Handling: Detect and fix negative sales quantities
neg_qty = df[df['Quantity'] < 0]
print('Rows with negative quantities:', neg_qty.shape[0])
# Fix: Remove negative quantities (often returns/refunds)
df_clean = df[df['Quantity'] >= 0]
Rows with negative quantities: 10624
# Debugging: MistakeGrouping by invoice number instead of date
wrong_group = df.groupby('Invoice')['Revenue'].sum()
print(wrong_group.head())
Invoice
536365    139.12
536366     22.20
536367    278.73
536368     70.05
536369     17.85
Name: Revenue, dtype: float64
# Pattern: Create a seasonality index (average sales by month)
seasonality_idx = monthly_revenue / monthly_revenue.mean()
print(seasonality_idx.round(2))
Month
1     0.69
2     0.61
3     0.84
4     0.61
5     0.89
6     0.85
7     0.84
8     0.84
9     1.26
10    1.32
11    1.80
12    1.46
Name: Revenue, dtype: float64
# Pattern: Compare product-level demand trends over time
top_products = df_clean.groupby('Description')['Quantity'].sum().sort_values(ascending=False).head(3).index
for prod in top_products:
    prod_daily = df_clean[df_clean['Description'] == prod].groupby(df_clean['InvoiceDate'].dt.date)['Quantity'].sum()
    plt.plot(prod_daily.index, prod_daily.values, label=prod)
plt.title('Daily Demand for Top 3 Products')
plt.xlabel('Date')
plt.ylabel('Quantity Sold')
plt.legend()
plt.show()
No description has been provided for this image
# End-to-End: Identify month with the highest demand surge and recommend action
peak_month = monthly_revenue.idxmax()
surge = monthly_revenue.max() - monthly_revenue.median()
print(f'Peak demand is in month {peak_month}. Surges by {surge:,.0f} units above typical demand.')
recommendation = f'Recommendation: Ensure stock and extra staff for month {peak_month}, as demand surges well above typical levels!'
print(recommendation)
Peak demand is in month 11. Surges by 774,561 units above typical demand.
Recommendation: Ensure stock and extra staff for month 11, as demand surges well above typical levels!
 

Found this useful?

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