Table of Content¶

  • Importing packages and Dataset
    • Packages
    • Importing dataset
  • Conducting initial data investigation
    • Findings
  • Classical techniques
    • Findings
  • Machine learning and deep learning techniques
    • XGBoost model
    • LSTM
    • Findings
  • Hybrid Model
    • Sequential Combination
    • Parallel Combination
    • Findings
  • Monthly Prediction
    • XGBoost Model
    • Auto ARIMA
    • Findings

Importing packages and Dataset¶

Packages¶

In [ ]:
!pip install --upgrade --force-reinstall --no-cache-dir numpy==1.23.5 pmdarima sktime tensorflow==2.12.0rc0 keras-tuner
In [ ]:
import itertools
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose, STL
from statsmodels.stats.diagnostic import acorr_ljungbox
import statsmodels.graphics.api as smgraphics
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.statespace.sarimax import SARIMAX
from scipy.stats import boxcox
from scipy.special import inv_boxcox
from pmdarima import auto_arima
import warnings
from sktime.forecasting.compose import (TransformedTargetForecaster, make_reduction)
from sktime.forecasting.model_selection import (ExpandingWindowSplitter, ForecastingGridSearchCV)
from sktime.performance_metrics.forecasting import MeanAbsolutePercentageError
from sktime.forecasting.trend import PolynomialTrendForecaster
from sktime.transformations.series.detrend import Deseasonalizer, Detrender
from sktime.forecasting.base import ForecastingHorizon
from xgboost import XGBRegressor
import numpy as np
import tensorflow as tf
from keras.models import Sequential
from keras.layers import Dense,LSTM, Dropout, InputLayer
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_absolute_percentage_error
from sklearn.preprocessing import MinMaxScaler
import keras_tuner as kt
from keras.regularizers import l1_l2
from keras.optimizers import Adam, SGD, RMSprop
from keras.callbacks import EarlyStopping

# Suppress specific warnings.
warnings.filterwarnings("ignore", category=FutureWarning)
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)

Importing dataset¶

In [ ]:
isbn = pd.read_excel("/content/ISBN List.xlsx", sheet_name=None)
uk_weekly = pd.read_excel("/content/UK Weekly Trended Timeline from 200101_202429.xlsx", sheet_name=None)
In [ ]:
isbn_Educational = pd.DataFrame(isbn["Y Childrens, YA & Educational"])
isbn_Trade = pd.DataFrame(isbn['T Adult Non-Fiction Trade'])
isbn_Specialist = pd.DataFrame(isbn['S Adult Non-Fiction Specialist'])
isbn_Fiction = pd.DataFrame(isbn['F - Adult Fiction'])
In [ ]:
uk_weekly_Educational = pd.DataFrame(uk_weekly["Y Children's, YA & Educational"])
uk_weekly_Fiction = pd.DataFrame(uk_weekly['F Adult Fiction'])
uk_weekly_Trade = pd.DataFrame(uk_weekly['T Adult Non-Fiction Trade'])
uk_weekly_Specialist = pd.DataFrame(uk_weekly['S Adult Non-Fiction Specialist'])
In [ ]:
isbn_names = {"isbn_Educational": isbn_Educational, "isbn_Trade": isbn_Trade, "isbn_Specialist": isbn_Specialist,
              "isbn_Fiction": isbn_Fiction}
uk_weekly_name = {"uk_weekly_Educational":uk_weekly_Educational, "uk_weekly_Trade": uk_weekly_Trade,
                  "uk_weekly_Specialist": uk_weekly_Specialist,"uk_weekly_Fiction": uk_weekly_Fiction}

Conducting initial data investigation¶

In [ ]:
def explore(dataframe_name):

  for i, data in dataframe_name.items():

    print(f"{' '.join(i.split('_'))} have the follwing properties")
    print("------------------------------------------------------------------------------------------")
    print(f"Shape \n {data.shape}")
    print("------------------------------------------------------------------------------------------")
    print(data.info())
    print("------------------------------------------------------------------------------------------")
    print(data.describe())
    print("******************************************************************************************")
In [ ]:
explore(uk_weekly_name)
uk weekly Educational have the follwing properties
------------------------------------------------------------------------------------------
Shape 
 (55286, 13)
------------------------------------------------------------------------------------------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 55286 entries, 0 to 55285
Data columns (total 13 columns):
 #   Column           Non-Null Count  Dtype         
---  ------           --------------  -----         
 0   ISBN             55286 non-null  int64         
 1   Title            55286 non-null  object        
 2   Author           50113 non-null  object        
 3   Interval         55286 non-null  int64         
 4   End Date         55286 non-null  datetime64[ns]
 5   Volume           55286 non-null  int64         
 6   Value            55286 non-null  float64       
 7   ASP              55193 non-null  float64       
 8   RRP              54856 non-null  float64       
 9   Binding          55286 non-null  object        
 10  Imprint          55286 non-null  object        
 11  Publisher Group  55286 non-null  object        
 12  Product Class    55286 non-null  object        
dtypes: datetime64[ns](1), float64(3), int64(3), object(6)
memory usage: 5.5+ MB
None
------------------------------------------------------------------------------------------
               ISBN       Interval                       End Date  \
count  5.528600e+04   55286.000000                          55286   
mean   9.780811e+12  200744.123720  2007-09-04 13:22:46.523170304   
min    9.780002e+12  200101.000000            2001-01-06 00:00:00   
25%    9.780441e+12  200318.000000            2003-05-03 00:00:00   
50%    9.780721e+12  200601.000000            2006-01-07 00:00:00   
75%    9.780753e+12  201023.000000            2010-06-05 00:00:00   
max    9.781904e+12  202429.000000            2024-07-20 00:00:00   
std    5.779893e+08     561.901387                            NaN   

              Volume          Value           ASP           RRP  
count   55286.000000   55286.000000  55193.000000  54856.000000  
mean      530.412564    2743.474619      5.556607      6.921442  
min      -269.000000   -1348.460000    -11.010000      0.100000  
25%        18.000000      90.835000      4.406400      4.990000  
50%       151.000000     784.520000      4.962700      6.500000  
75%       493.000000    2435.630000      5.990000      7.990000  
max    193645.000000  483767.750000     40.000000     25.000000  
std      2314.946914   10668.029749      2.709133      3.219413  
******************************************************************************************
uk weekly Trade have the follwing properties
------------------------------------------------------------------------------------------
Shape 
 (65344, 13)
------------------------------------------------------------------------------------------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 65344 entries, 0 to 65343
Data columns (total 13 columns):
 #   Column           Non-Null Count  Dtype         
---  ------           --------------  -----         
 0   ISBN             65344 non-null  int64         
 1   Title            65344 non-null  object        
 2   Author           60655 non-null  object        
 3   Interval         65344 non-null  int64         
 4   End Date         65344 non-null  datetime64[ns]
 5   Volume           65344 non-null  int64         
 6   Value            65344 non-null  float64       
 7   ASP              65274 non-null  float64       
 8   RRP              65210 non-null  float64       
 9   Binding          65344 non-null  object        
 10  Imprint          65344 non-null  object        
 11  Publisher Group  65344 non-null  object        
 12  Product Class    65344 non-null  object        
dtypes: datetime64[ns](1), float64(3), int64(3), object(6)
memory usage: 6.5+ MB
None
------------------------------------------------------------------------------------------
               ISBN       Interval                       End Date  \
count  6.534400e+04   65344.000000                          65344   
mean   9.780467e+12  200884.679603  2009-01-30 21:32:22.360430848   
min    9.780003e+12  200101.000000            2001-01-06 00:00:00   
25%    9.780140e+12  200347.000000            2003-11-22 00:00:00   
50%    9.780416e+12  200731.000000            2007-08-04 00:00:00   
75%    9.780718e+12  201317.000000            2013-04-27 00:00:00   
max    9.781904e+12  202429.000000            2024-07-20 00:00:00   
std    3.994394e+08     611.839318                            NaN   

             Volume         Value           ASP           RRP  
count  65344.000000  6.534400e+04  65274.000000  65210.000000  
mean     376.208849  3.081678e+03      9.206646     12.781906  
min      -74.000000 -4.372600e+02    -77.460000      1.000000  
25%        7.000000  5.921750e+01      6.767075      8.990000  
50%       38.000000  3.285200e+02      7.986300     10.990000  
75%      239.000000  1.902920e+03     10.470900     15.990000  
max    80620.000000  1.056257e+06     32.010000     30.000000  
std     1591.597425  1.664461e+04      4.683351      6.206623  
******************************************************************************************
uk weekly Specialist have the follwing properties
------------------------------------------------------------------------------------------
Shape 
 (32827, 13)
------------------------------------------------------------------------------------------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 32827 entries, 0 to 32826
Data columns (total 13 columns):
 #   Column           Non-Null Count  Dtype         
---  ------           --------------  -----         
 0   ISBN             32827 non-null  int64         
 1   Title            32827 non-null  object        
 2   Author           28077 non-null  object        
 3   Interval         32827 non-null  int64         
 4   End Date         32827 non-null  datetime64[ns]
 5   Volume           32827 non-null  int64         
 6   Value            32827 non-null  float64       
 7   ASP              32731 non-null  float64       
 8   RRP              27429 non-null  float64       
 9   Binding          32827 non-null  object        
 10  Imprint          32827 non-null  object        
 11  Publisher Group  32827 non-null  object        
 12  Product Class    32827 non-null  object        
dtypes: datetime64[ns](1), float64(3), int64(3), object(6)
memory usage: 3.3+ MB
None
------------------------------------------------------------------------------------------
               ISBN       Interval                       End Date  \
count  3.282700e+04   32827.000000                          32827   
mean   9.780743e+12  200450.247479  2004-09-24 01:00:03.180308864   
min    9.780003e+12  200101.000000            2001-01-06 00:00:00   
25%    9.780341e+12  200218.000000            2002-05-04 00:00:00   
50%    9.780672e+12  200340.000000            2003-10-04 00:00:00   
75%    9.780765e+12  200602.000000            2006-01-14 00:00:00   
max    9.781904e+12  202429.000000            2024-07-20 00:00:00   
std    5.613454e+08     342.780391                            NaN   

             Volume         Value           ASP           RRP  
count  32827.000000  32827.000000  32731.000000  27429.000000  
mean      87.351296   1176.246431     13.479478     16.302991  
min      -86.000000   -863.620000     -5.765000      2.950000  
25%        5.000000     49.545000      8.429300      9.990000  
50%       41.000000    456.540000     12.020600     12.990000  
75%      114.000000   1451.050000     17.132050     19.950000  
max     4378.000000  39125.560000     46.690000     69.990000  
std      143.989186   2137.410382      7.445967     10.155029  
******************************************************************************************
uk weekly Fiction have the follwing properties
------------------------------------------------------------------------------------------
Shape 
 (73767, 13)
------------------------------------------------------------------------------------------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 73767 entries, 0 to 73766
Data columns (total 13 columns):
 #   Column           Non-Null Count  Dtype         
---  ------           --------------  -----         
 0   ISBN             73767 non-null  int64         
 1   Title            73767 non-null  object        
 2   Author           73500 non-null  object        
 3   Interval         73767 non-null  int64         
 4   End Date         73767 non-null  datetime64[ns]
 5   Volume           73767 non-null  int64         
 6   Value            73767 non-null  float64       
 7   ASP              73683 non-null  float64       
 8   RRP              73767 non-null  float64       
 9   Binding          73767 non-null  object        
 10  Imprint          73767 non-null  object        
 11  Publisher Group  73767 non-null  object        
 12  Product Class    73767 non-null  object        
dtypes: datetime64[ns](1), float64(3), int64(3), object(6)
memory usage: 7.3+ MB
None
------------------------------------------------------------------------------------------
               ISBN       Interval                       End Date  \
count  7.376700e+04   73767.000000                          73767   
mean   9.780393e+12  200877.213347  2009-01-03 22:50:01.049249536   
min    9.780002e+12  200101.000000            2001-01-06 00:00:00   
25%    9.780140e+12  200410.000000            2004-03-06 00:00:00   
50%    9.780341e+12  200735.000000            2007-09-01 00:00:00   
75%    9.780554e+12  201229.000000            2012-07-21 00:00:00   
max    9.781860e+12  202429.000000            2024-07-20 00:00:00   
std    3.179225e+08     592.264956                            NaN   

             Volume          Value           ASP           RRP  
count  73767.000000   73767.000000  73683.000000  73767.000000  
mean     381.227473    2448.109140      7.157384      9.566535  
min      -20.000000     -96.420000     -4.010000      2.500000  
25%        9.000000      62.270000      6.200400      7.990000  
50%       55.000000     383.900000      6.892600      8.990000  
75%      192.000000    1331.300000      7.730400      9.990000  
max    51175.000000  385543.990000     60.000000     25.000000  
std     1541.015144    9746.887969      2.323545      3.012778  
******************************************************************************************
In [ ]:
def initial_processing(dataframe_name):

  for i, data in dataframe_name.items():


    data.set_index("End Date", inplace = True)
    data.sort_index(inplace = True)
    data["ISBN"] = data["ISBN"].astype("str")
    data.resample("W").sum().fillna(0, inplace = True)
In [ ]:
initial_processing(uk_weekly_name)
In [ ]:
def plot_isbn(dataframe_name):
  data2024_07_01 = {}
  unique_after2024_07_01 = []
  cutoff = pd.Timestamp('2024-07-01')

  for i, data in dataframe_name.items():

    for isbncutoff in data[data.index > cutoff]["ISBN"].unique():

      unique_after2024_07_01.append(isbncutoff)
      plt.figure(figsize=(12,8))
      plt.plot(data[data["ISBN"] == isbncutoff]["Volume"])
      plt.title(f"Sales Volume Over Time for ISBN {isbncutoff}")
      plt.xlabel("Sales Date")
      plt.ylabel("Volume")
      plt.show()

  print(unique_after2024_07_01)
In [ ]:
plot_isbn(uk_weekly_name)
['9781841462400', '9780006647553', '9780440864141', '9780241003008', '9781841460406', '9780744523232', '9781841462301', '9780440864554', '9781841461502', '9780752844299', '9781841460307', '9781841462509', '9780752846576', '9780099422587', '9780340696767', '9780099285823', '9780552145954', '9780552997034', '9780593048153', '9780140275421', '9780091816971', '9781841150437', '9780006531203', '9780140281293', '9780091867775', '9780749395698', '9780140259506', '9780719559792', '9780140276619', '9780340766057', '9780099286578', '9780099428558', '9780140294231', '9780224060875', '9780330355667', '9780340786055', '9780099286387', '9780552998482', '9780261103252', '9780099771517', '9780349114033', '9780552998727', '9780552997348', '9780006514213', '9780140276336', '9780140285215', '9780552998000', '9780747268161', '9780140295962', '9780552998444', '9780349113609', '9780349112763', '9780099244721', '9780749397548', '9780006512134', '9780722532935', '9780006514091', '9780007101887', '9780552145060', '9780006550433', '9780552145053']
In [ ]:
books = ["very hungry caterpillar", "alchemist"]
def filter_books(isbn_books, weekly_sales, books):
  lookedup_isbn = {}

  for i in books:
      for (key1, data1), (key2, data2) in zip(isbn_books.items(), weekly_sales.items()):

        data1["ISBN"] = data1["ISBN"].astype("str")
        result = data1[data1["Title"].str.lower().str.contains(i)]["ISBN"]
        if not result.empty:
          data2_result = data2[data2["ISBN"].isin(list(result))]
          data2_result.index = data2_result.index + pd.Timedelta(days=1)
          lookedup_isbn[i.split()[-1]] = data2_result.groupby(data2_result.index).sum().asfreq('W').fillna(0)["Volume"]


  return lookedup_isbn
In [ ]:
selected_books = filter_books(isbn_names, uk_weekly_name, books)

Findings¶

📘 UK Weekly Book Sales - Dataset Summary¶

📂 Dataset Shapes¶

Category Rows Columns
Educational 55,286 13
Trade 65,344 13
Specialist 32,827 13
Fiction 73,767 13

🔍 Columns for all four books¶

  • ISBN: Book identifier (int)
  • Title: Book title (str)
  • Author: Book author (nullable)
  • Interval: Weekly time index (int)
  • End Date: Week end date (datetime)
  • Volume: Units sold (int)
  • Value: Sales revenue (float)
  • ASP: Average selling price (float, nullable)
  • RRP: Recommended retail price (float, nullable)
  • Binding: Format type (str)
  • Imprint: Publishing imprint (str)
  • Publisher Group: Group name (str)
  • Product Class: Product category (str)

🧼 Missing Values¶

Column Educational Trade Specialist Fiction
Author 5,173 4,689 4,750 267
ASP 93 70 96 84
RRP 430 134 5,398 0

📊 Key Averages¶

Metric Educational Trade Specialist Fiction
Volume 530 376 87 381
Value (£) 2,743 3,082 1,176 2,448
ASP (£) 5.56 9.21 13.48 7.16
RRP (£) 6.92 12.78 16.30 9.57

📅 Date Range¶

  • All datasets: 2001-01-06 to 2024-07-20

⚠️ Notes¶

  • For this project, the focus will be on Volume and End Date, used as the time series index for analysis.
  • End Date has been set as the index and resampled to weekly intervals.
  • Missing weeks are filled with zero sales to indicate no recorded transactions.

Plot Findings¶

  • A total of 61 books have recorded sales beyond 2024-07-01. Plotting these books revealed several key characteristics:
  • There is a clear downward trend in sales from 2000 to around 2012.
  • After 2012, sales stabilize at a lower level, with relatively flat trends and some fluctuations.
  • Some books reached a stable phase earlier, with a few stabilizing as early as 2004.
  • Most books showed:
    • A strong initial release period.
    • A subsequent decline in popularity over time.
  • Certain books display seasonal or cyclical patterns:
    • Likely linked to academic calendars.
    • These cycles become less pronounced over time, possibly due to:
      • Reduced academic use.
      • Declining baseline demand.
      • Or a combination of both.

Classical techniques¶

In [ ]:
alchemist = pd.DataFrame(selected_books['alchemist'])
caterpillar = pd.DataFrame(selected_books['caterpillar'])
In [ ]:
print(type(alchemist["Volume"].index))
print(alchemist.index.freq)
print(alchemist.index.inferred_freq)
<class 'pandas.core.indexes.datetimes.DatetimeIndex'>
<Week: weekday=6>
W-SUN
In [ ]:
cutoff = pd.Timestamp('2012-01-01')
alchemist_cutoff = alchemist[alchemist.index > cutoff]
caterpillar_cutoff = caterpillar[caterpillar.index > cutoff]
In [ ]:
two_books = {"The Alchemist": alchemist_cutoff, "The Very Hungry Caterpillar": caterpillar_cutoff}
explore(two_books)
The Alchemist have the follwing properties
------------------------------------------------------------------------------------------
Shape 
 (655, 1)
------------------------------------------------------------------------------------------
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 655 entries, 2012-01-08 to 2024-07-21
Freq: W-SUN
Data columns (total 1 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   Volume  655 non-null    float64
dtypes: float64(1)
memory usage: 10.2 KB
None
------------------------------------------------------------------------------------------
            Volume
count   655.000000
mean    528.102290
std     227.965588
min       0.000000
25%     415.500000
50%     508.000000
75%     606.000000
max    2201.000000
******************************************************************************************
The Very Hungry Caterpillar have the follwing properties
------------------------------------------------------------------------------------------
Shape 
 (655, 1)
------------------------------------------------------------------------------------------
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 655 entries, 2012-01-08 to 2024-07-21
Freq: W-SUN
Data columns (total 1 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   Volume  655 non-null    float64
dtypes: float64(1)
memory usage: 10.2 KB
None
------------------------------------------------------------------------------------------
            Volume
count   655.000000
mean   1348.909924
std     710.689315
min       0.000000
25%     723.000000
50%    1324.000000
75%    1758.500000
max    3905.000000
******************************************************************************************
In [ ]:
plt.figure(figsize=(15,8))
alchemist_cutoff["Volume"].plot(label='Alchemist')
caterpillar_cutoff["Volume"].plot(label='Caterpillar')
plt.title('Sales Volume for both Alchemist and Caterpillar')
plt.xlabel('Date')
plt.ylabel('Volume')
plt.legend()
plt.show()
In [ ]:
def decomposition(data, pd):

  data_stl = STL(data , period=pd)
  data_fit = data_stl.fit()
  residual = data_fit.resid
  #data_fit.plot();
  print('Ljung-Box test output\n', acorr_ljungbox(residual), '...\n')
  return residual
In [ ]:
print("Decomposition for The Very Hungry Caterpillar")
residual_cat = decomposition(caterpillar_cutoff, 52)
print("\n Decomposition for The Alchemist")
residual_alch = decomposition(alchemist_cutoff, 52)
Decomposition for The Very Hungry Caterpillar
Ljung-Box test output
        lb_stat      lb_pvalue
1   370.471831   1.477278e-82
2   563.416272  4.525964e-123
3   655.510867  9.300685e-142
4   703.722530  5.447512e-151
5   735.165914  1.221569e-156
6   745.231614  1.044323e-157
7   746.925241  5.238363e-157
8   747.070784  5.223687e-156
9   747.322418  4.603664e-155
10  752.384656  3.530257e-155 ...


 Decomposition for The Alchemist
Ljung-Box test output
        lb_stat      lb_pvalue
1   322.397402   4.352123e-72
2   493.431912  7.122324e-108
3   598.685446  1.942542e-129
4   669.332329  1.521385e-143
5   712.116376  1.178602e-151
6   735.396404  1.390001e-155
7   743.813088  2.457366e-156
8   745.613803  1.076019e-155
9   748.310139  2.822442e-155
10  763.421832  1.500879e-157 ...

In [ ]:
def acf_pacf_plot(data):
  smgraphics.tsa.plot_acf(data, lags=52);
  smgraphics.tsa.plot_pacf(data, lags=52);
In [ ]:
print("ACF and PACF Plot for The Very Hungry Caterpillar using Decomposition Residual")
acf_pacf_plot(residual_cat)
plt.show()
print("\n ACF and PACF Plot for The Alchemist using Decomposition Residual")
acf_pacf_plot(residual_alch)
plt.show()
ACF and PACF Plot for The Very Hungry Caterpillar using Decomposition Residual
 ACF and PACF Plot for The Alchemist using Decomposition Residual
In [ ]:
print("ACF and PACF Plot for The Very Hungry Caterpillar using Volume")
acf_pacf_plot(caterpillar_cutoff)
plt.show()
print("\n ACF and PACF Plot for The Alchemist using Volume")
acf_pacf_plot(alchemist_cutoff)
plt.show()
ACF and PACF Plot for The Very Hungry Caterpillar using Volume