Problem Statement¶

Business Context¶

A sales forecast is a prediction of future sales revenue based on historical data, industry trends, and the status of the current sales pipeline. Businesses use the sales forecast to estimate weekly, monthly, quarterly, and annual sales totals. A company needs to make an accurate sales forecast as it adds value across an organization and helps the different verticals to chalk out their future course of action.

Forecasting helps an organization plan its sales operations by region and provides valuable insights to the supply chain team regarding the procurement of goods and materials. An accurate sales forecast process has many benefits which include improved decision-making about the future and reduction of sales pipeline and forecast risks. Moreover, it helps to reduce the time spent in planning territory coverage and establish benchmarks that can be used to assess trends in the future.

Objective¶

SuperKart is a retail chain operating supermarkets and food marts across various tier cities, offering a wide range of products. To optimize its inventory management and make informed decisions around regional sales strategies, SuperKart wants to accurately forecast the sales revenue of its outlets for the upcoming quarter.

To operationalize these insights at scale, the company has partnered with a data science firm—not just to build a predictive model based on historical sales data, but to develop and deploy a robust forecasting solution that can be integrated into SuperKart’s decision-making systems and used across its network of stores.

Data Description¶

The data contains the different attributes of the various products and stores.The detailed data dictionary is given below.

  • Product_Id - unique identifier of each product, each identifier having two letters at the beginning followed by a number.
  • Product_Weight - weight of each product
  • Product_Sugar_Content - sugar content of each product like low sugar, regular and no sugar
  • Product_Allocated_Area - ratio of the allocated display area of each product to the total display area of all the products in a store
  • Product_Type - broad category for each product like meat, snack foods, hard drinks, dairy, canned, soft drinks, health and hygiene, baking goods, bread, breakfast, frozen foods, fruits and vegetables, household, seafood, starchy foods, others
  • Product_MRP - maximum retail price of each product
  • Store_Id - unique identifier of each store
  • Store_Establishment_Year - year in which the store was established
  • Store_Size - size of the store depending on sq. feet like high, medium and low
  • Store_Location_City_Type - type of city in which the store is located like Tier 1, Tier 2 and Tier 3. Tier 1 consists of cities where the standard of living is comparatively higher than its Tier 2 and Tier 3 counterparts.
  • Store_Type - type of store depending on the products that are being sold there like Departmental Store, Supermarket Type 1, Supermarket Type 2 and Food Mart
  • Product_Store_Sales_Total - total revenue generated by the sale of that particular product in that particular store

Installing and Importing the necessary libraries¶

In [1]:
#Installing the libraries with the specified versions
!pip install numpy==2.0.2 pandas==2.2.2 scikit-learn==1.6.1 matplotlib==3.10.0 seaborn==0.13.2 joblib==1.4.2 xgboost==2.1.4 requests==2.32.4 huggingface_hub==0.34.0 -q
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 301.8/301.8 kB 5.0 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 223.6/223.6 MB 1.6 MB/s eta 0:00:00
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 558.7/558.7 kB 14.6 MB/s eta 0:00:00

Note:

  • After running the above cell, kindly restart the notebook kernel (for Jupyter Notebook) or runtime (for Google Colab) and run all cells sequentially from the next cell.

  • On executing the above line of code, you might see a warning regarding package dependencies. This error message can be ignored as the above code ensures that all necessary libraries and their dependencies are maintained to successfully execute the code in this notebook.

In [1]:
import warnings
warnings.filterwarnings("ignore")

# Libraries to help with reading and manipulating data
import numpy as np
import pandas as pd

# For splitting the dataset
from sklearn.model_selection import train_test_split

# Libaries to help with data visualization
import matplotlib.pyplot as plt
import seaborn as sns

# Removes the limit for the number of displayed columns
pd.set_option("display.max_columns", None)
# Sets the limit for the number of displayed rows
pd.set_option("display.max_rows", 100)


# Libraries different ensemble classifiers
from sklearn.ensemble import (
    BaggingRegressor,
    RandomForestRegressor,
    AdaBoostRegressor,
    GradientBoostingRegressor,
)
from xgboost import XGBRegressor
from sklearn.tree import DecisionTreeRegressor

# Libraries to get different metric scores
from sklearn.metrics import (
    confusion_matrix,
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    mean_squared_error,
    mean_absolute_error,
    r2_score,
    mean_absolute_percentage_error
)

# To create the pipeline
from sklearn.compose import make_column_transformer
from sklearn.pipeline import make_pipeline,Pipeline

# To tune different models and standardize
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler,OneHotEncoder

# To serialize the model
import joblib

# os related functionalities
import os

# API request
import requests

# for hugging face space authentication to upload files
from huggingface_hub import login, HfApi

Loading the dataset¶

In [2]:
# mount google drive
from google.colab import drive
drive.mount('/content/drive')
Mounted at /content/drive
In [3]:
file_path = '/content/drive/MyDrive/Colab Notebooks/'

kart = pd.read_csv(file_path + 'SuperKart.csv')
data = kart.copy()

Data Overview¶

In [4]:
data.head()
Out[4]:
Product_Id Product_Weight Product_Sugar_Content Product_Allocated_Area Product_Type Product_MRP Store_Id Store_Establishment_Year Store_Size Store_Location_City_Type Store_Type Product_Store_Sales_Total
0 FD6114 12.66 Low Sugar 0.027 Frozen Foods 117.08 OUT004 2009 Medium Tier 2 Supermarket Type2 2842.40
1 FD7839 16.54 Low Sugar 0.144 Dairy 171.43 OUT003 1999 Medium Tier 1 Departmental Store 4830.02
2 FD5075 14.28 Regular 0.031 Canned 162.08 OUT001 1987 High Tier 2 Supermarket Type1 4130.16
3 FD8233 12.10 Low Sugar 0.112 Baking Goods 186.31 OUT001 1987 High Tier 2 Supermarket Type1 4132.18
4 NC1180 9.57 No Sugar 0.010 Health and Hygiene 123.67 OUT002 1998 Small Tier 3 Food Mart 2279.36
In [5]:
data.tail()
Out[5]:
Product_Id Product_Weight Product_Sugar_Content Product_Allocated_Area Product_Type Product_MRP Store_Id Store_Establishment_Year Store_Size Store_Location_City_Type Store_Type Product_Store_Sales_Total
8758 NC7546 14.80 No Sugar 0.016 Health and Hygiene 140.53 OUT004 2009 Medium Tier 2 Supermarket Type2 3806.53
8759 NC584 14.06 No Sugar 0.142 Household 144.51 OUT004 2009 Medium Tier 2 Supermarket Type2 5020.74
8760 NC2471 13.48 No Sugar 0.017 Health and Hygiene 88.58 OUT001 1987 High Tier 2 Supermarket Type1 2443.42
8761 NC7187 13.89 No Sugar 0.193 Household 168.44 OUT001 1987 High Tier 2 Supermarket Type1 4171.82
8762 FD306 14.73 Low Sugar 0.177 Snack Foods 224.93 OUT002 1998 Small Tier 3 Food Mart 2186.08
In [6]:
# understand shape of the dataset
print(f"There are {data.shape[0]} rows without header and {data.shape[1]} columns.")
There are 8763 rows without header and 12 columns.
In [7]:
# check the data types of the columns for the dataset
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8763 entries, 0 to 8762
Data columns (total 12 columns):
 #   Column                     Non-Null Count  Dtype  
---  ------                     --------------  -----  
 0   Product_Id                 8763 non-null   object 
 1   Product_Weight             8763 non-null   float64
 2   Product_Sugar_Content      8763 non-null   object 
 3   Product_Allocated_Area     8763 non-null   float64
 4   Product_Type               8763 non-null   object 
 5   Product_MRP                8763 non-null   float64
 6   Store_Id                   8763 non-null   object 
 7   Store_Establishment_Year   8763 non-null   int64  
 8   Store_Size                 8763 non-null   object 
 9   Store_Location_City_Type   8763 non-null   object 
 10  Store_Type                 8763 non-null   object 
 11  Product_Store_Sales_Total  8763 non-null   float64
dtypes: float64(4), int64(1), object(7)
memory usage: 821.7+ KB
In [8]:
# statistical summary of the data
data.describe(include='all').T
Out[8]:
count unique top freq mean std min 25% 50% 75% max
Product_Id 8763 8763 FD306 1 NaN NaN NaN NaN NaN NaN NaN
Product_Weight 8763.0 NaN NaN NaN 12.653792 2.21732 4.0 11.15 12.66 14.18 22.0
Product_Sugar_Content 8763 4 Low Sugar 4885 NaN NaN NaN NaN NaN NaN NaN
Product_Allocated_Area 8763.0 NaN NaN NaN 0.068786 0.048204 0.004 0.031 0.056 0.096 0.298
Product_Type 8763 16 Fruits and Vegetables 1249 NaN NaN NaN NaN NaN NaN NaN
Product_MRP 8763.0 NaN NaN NaN 147.032539 30.69411 31.0 126.16 146.74 167.585 266.0
Store_Id 8763 4 OUT004 4676 NaN NaN NaN NaN NaN NaN NaN
Store_Establishment_Year 8763.0 NaN NaN NaN 2002.032751 8.388381 1987.0 1998.0 2009.0 2009.0 2009.0
Store_Size 8763 3 Medium 6025 NaN NaN NaN NaN NaN NaN NaN
Store_Location_City_Type 8763 3 Tier 2 6262 NaN NaN NaN NaN NaN NaN NaN
Store_Type 8763 4 Supermarket Type2 4676 NaN NaN NaN NaN NaN NaN NaN
Product_Store_Sales_Total 8763.0 NaN NaN NaN 3464.00364 1065.630494 33.0 2761.715 3452.34 4145.165 8000.0
In [9]:
# check for duplicates
data.duplicated().sum()
Out[9]:
np.int64(0)
In [10]:
# find missing values
data.isnull().sum()
Out[10]:
0
Product_Id 0
Product_Weight 0
Product_Sugar_Content 0
Product_Allocated_Area 0
Product_Type 0
Product_MRP 0
Store_Id 0
Store_Establishment_Year 0
Store_Size 0
Store_Location_City_Type 0
Store_Type 0
Product_Store_Sales_Total 0

Exploratory Data Analysis (EDA)¶

In [11]:
data.shape
Out[11]:
(8763, 12)

Univariate Analysis¶

In [12]:
# function to plot a boxplot and a histogram along the same scale

def histogram_boxplot(data, feature, figsize=(12, 7), kde=False, bins=None):
    """
    Boxplot and histogram combined

    data: dataframe
    feature: dataframe column
    figsize: size of figure (default (12,7))
    kde: whether to show the density curve (default False)
    bins: number
    """
    f2, (ax_box2, ax_hist2) = plt.subplots(
        nrows=2,  # Number of rows of the subplot grid= 2
        sharex=True,  # x-axis will be shared among all subplots
        gridspec_kw={"height_ratios": (0.25, 0.74)},
        figsize=figsize,
    )  # creating the 2 subplots
    sns.boxplot(
        data=data, x=feature, ax=ax_box2, showmeans=True, color="violet"
    )  # boxplot will be created and a star will indicate the mean value of the column
    sns.histplot(
        data=data, x=feature, kde=kde, ax=ax_hist2, bins=bins, palette="winter"
    ) if bins else sns.histplot(
        data=data, x=feature, kde=kde, ax=ax_hist2
    ) # for histogram
    ax_hist2.axvline(
        data[feature].mean(), color="green", linestyle="--"
    )  # Add a vertical line to the mean of the column / add mean to the histogram
    ax_hist2.axvline(
        data[feature].median(), color="black", linestyle="-"
    )  # Add a vertical line to the median of the column / add median to the histogram
In [13]:
# product weight
histogram_boxplot(data, 'Product_Weight')
No description has been provided for this image
In [14]:
# product allocated area
histogram_boxplot(data, 'Product_Allocated_Area')
No description has been provided for this image
In [15]:
# product mrp
histogram_boxplot(data, 'Product_MRP')
No description has been provided for this image
In [16]:
# product store sales total
histogram_boxplot(data, 'Product_Store_Sales_Total')
No description has been provided for this image
In [17]:
# barplots function
def labeled_barplot(data, feature, perc=False, n=None):
    """
    Barplot

    data: dataframe
    feature: dataframe column
    perc: whether to show percentages instead of count (default False)
    n: displays the top n category levels (default None, i.e., display all levels)
    """
    total = len(data[feature])  # length of the column
    count = data[feature].nunique()
    if n is None:
        plt.figure(figsize=(count + 1, 5))
    else:
        plt.figure(figsize=(n + 1, 5))
    plt.xticks(rotation=90, fontsize=10)
    ax = sns.countplot(
        data=data,
        x=feature,
        palette="Paired",
        order=data[feature].value_counts().index[:n].sort_values(),
    )
    for p in ax.patches:
        if perc == True:
            label = "{:.1f}%".format(
                100 * p.get_height() / total
            ) # percentage of each class of the category
        else:
            label = p.get_height() # count of each level of the category

        x = p.get_x() + p.get_width() / 2  # width of the plot
        y = p.get_height() # height of the plot
        ax.annotate(
            label,
            (x, y),
            ha="center",
            va="center",
            size=12,
            xytext=(0, 5),
            textcoords="offset points",
        )  # annotate the percentage
    plt.show() # show the plot
In [18]:
# product sugar content
labeled_barplot(data, 'Product_Sugar_Content', perc=True)
No description has been provided for this image
In [19]:
# product type
labeled_barplot(data, 'Product_Type', perc=True)
No description has been provided for this image
In [20]:
# store id
labeled_barplot(data, 'Store_Id', perc=True)
No description has been provided for this image
In [21]:
# store size
labeled_barplot(data, 'Store_Size', perc=True)
No description has been provided for this image
In [22]:
# store location city type
labeled_barplot(data, 'Store_Location_City_Type', perc=True)
No description has been provided for this image

Bivariate Analysis¶

In [23]:
# correlation matrix
cols_list = data.select_dtypes(include=np.number).columns.tolist()
plt.figure(figsize=(16, 6))
sns.heatmap(data[cols_list].corr(), annot=True, vmin=-1, vmax=1, fmt=".2f", cmap="Spectral")
plt.show()
No description has been provided for this image
In [24]:
# check distribution of product store sales total with numeric columns
plt.figure(figsize=[10, 8])
sns.scatterplot(x='Product_Weight', y='Product_Store_Sales_Total', data=data)
plt.show()
No description has been provided for this image
In [25]:
plt.figure(figsize=[10, 8])
sns.scatterplot(x='Product_Allocated_Area', y='Product_Store_Sales_Total', data=data)
plt.show()
No description has been provided for this image
In [26]:
plt.figure(figsize=[10, 8])
sns.scatterplot(x='Product_MRP', y='Product_Store_Sales_Total', data=data)
plt.show()
No description has been provided for this image
In [27]:
# see which product type the company is generating most of the revenue
revenue_df1 = data.groupby(['Product_Type'], as_index=False)[
    'Product_Store_Sales_Total'
    ].sum()
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
a = sns.barplot(x=revenue_df1.Product_Type, y=revenue_df1.Product_Store_Sales_Total)
a.set_xlabel("Product Type")
a.set_ylabel("Revenue")
a.set_title("Product Type vs Revenue")
plt.show()
No description has been provided for this image
In [28]:
revenue_df2 = data.groupby(['Product_Sugar_Content'], as_index=False)[
    'Product_Store_Sales_Total'
    ].sum()
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
b = sns.barplot(x=revenue_df2.Product_Sugar_Content, y=revenue_df2.Product_Store_Sales_Total)
b.set_xlabel("Product Sugar Content")
b.set_ylabel("Revenue")
b.set_title("Product Sugar Content vs Revenue")
plt.show()
No description has been provided for this image
In [29]:
# which stores and locations the revenue generated more
store_revenue_df = data.groupby(['Store_Id'], as_index=False)[
    'Product_Store_Sales_Total'
    ].sum()
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
rate = sns.barplot(x=store_revenue_df.Store_Id, y=store_revenue_df.Product_Store_Sales_Total)
rate.set_xlabel("Store Id")
rate.set_ylabel("Revenue")
rate.set_title("Store Id vs Revenue")
plt.show()
No description has been provided for this image
In [30]:
revenue_df3 = data.groupby(['Store_Location_City_Type'], as_index=False)[
    'Product_Store_Sales_Total'
    ].sum()
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
city = sns.barplot(x=revenue_df3.Store_Location_City_Type, y=revenue_df3.Product_Store_Sales_Total)
city.set_xlabel("Store Location City Type")
city.set_ylabel("Revenue")
city.set_title("Store Location City Type vs Revenue")
plt.show()
No description has been provided for this image
In [31]:
revenue_df4 = data.groupby(['Store_Size'], as_index=False)[
    'Product_Store_Sales_Total'
    ].sum()
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
size = sns.barplot(x=revenue_df4.Store_Size, y=revenue_df4.Product_Store_Sales_Total)
size.set_xlabel("Store Size")
size.set_ylabel("Revenue")
size.set_title("Store Size vs Revenue")
plt.show()
No description has been provided for this image
In [32]:
revenue_df5 = data.groupby(['Store_Type'], as_index=False)[
    'Product_Store_Sales_Total'
    ].sum()
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
type = sns.barplot(x=revenue_df5.Store_Type, y=revenue_df5.Product_Store_Sales_Total)
type.set_xlabel("Store Type")
type.set_ylabel("Revenue")
type.set_title("Store Type vs Revenue")
plt.show()
No description has been provided for this image
In [33]:
# Product_Store_Sale_Total with other categorical columns
plt.figure(figsize=[10, 8])
sns.boxplot(x='Store_Id', y='Product_Store_Sales_Total', data=data)
plt.xticks(rotation=90)
plt.title("Boxplot - Store_Id vs Product_Store_Sales_Total")
plt.xlabel("Store Id")
plt.ylabel("Product_Store_Sales_Total")
plt.show()
No description has been provided for this image
In [34]:
plt.figure(figsize=[10, 8])
sns.boxplot(x='Store_Size', y='Product_Store_Sales_Total', data=data)
plt.xticks(rotation=90)
plt.title("Boxplot - Store_Size vs Product_Store_Sales_Total")
plt.xlabel("Stores")
plt.ylabel("Product_Store_Sales_Total")
plt.show()
No description has been provided for this image
In [35]:
# relationship b/t the other columns
plt.figure(figsize=[10, 8])
sns.boxplot(data=data, x='Store_Location_City_Type', y='Product_Weight', hue='Store_Location_City_Type')
plt.xticks(rotation=90)
plt.title("Boxplot - Store_Location_City_Type vs Product_Weight")
plt.xlabel("Store Location City Type")
plt.ylabel("Product_Weight")
plt.show()
No description has been provided for this image
In [36]:
# weight of the product and sugar content
plt.figure(figsize=[10, 8])
sns.boxplot(data=data, x='Product_Sugar_Content', y='Product_Weight', hue='Product_Sugar_Content')
plt.xticks(rotation=90)
plt.title("Boxplot - Product_Sugar_Content vs Product_Weight")
plt.xlabel("Product Sugar Content")
plt.ylabel("Product_Weight")
plt.show()
No description has been provided for this image
In [37]:
# sugar content of different product types
plt.figure(figsize=[10, 8])
sns.heatmap(
    pd.crosstab(data["Product_Sugar_Content"], data["Product_Type"]), annot=True,
    fmt="g",
    cmap="Blues",
)
plt.xticks(rotation=90)
plt.title("Product_Sugar_Content vs Product_Type")
plt.xlabel("Product Sugar Content")
plt.ylabel("Product_Type")
plt.show()
No description has been provided for this image
In [38]:
# each of the product type sold vs each store name
plt.figure(figsize=[10, 8])
sns.heatmap(
    pd.crosstab(data["Store_Type"], data["Product_Type"]), annot=True,
    fmt="g",
    cmap="Blues",
)
plt.xticks(rotation=90)
plt.title("Store_Type vs Product_Type")
plt.xlabel("Store_Type")
plt.ylabel("Product_Type")
plt.show()
No description has been provided for this image
In [39]:
# product types have different prices. analyze them.
plt.figure(figsize=[10, 8])
sns.boxplot(data=data, x='Product_Type', y='Product_MRP', hue='Product_Type')
plt.xticks(rotation=90)
plt.title("Boxplot - Product_Type vs Product_MRP")
plt.xlabel("Product_Type")
plt.ylabel("Product_MRP")
plt.show()
No description has been provided for this image
In [40]:
# drive deeper and make a detailed analysis of the each of the stores

# OUT001
data.loc[data['Store_Id'] == 'OUT001'].describe(include='all').T
Out[40]:
count unique top freq mean std min 25% 50% 75% max
Product_Id 1586 1586 NC7187 1 NaN NaN NaN NaN NaN NaN NaN
Product_Weight 1586.0 NaN NaN NaN 13.458865 2.064975 6.16 12.0525 13.96 14.95 17.97
Product_Sugar_Content 1586 4 Low Sugar 845 NaN NaN NaN NaN NaN NaN NaN
Product_Allocated_Area 1586.0 NaN NaN NaN 0.068768 0.047131 0.004 0.033 0.0565 0.094 0.295
Product_Type 1586 16 Snack Foods 202 NaN NaN NaN NaN NaN NaN NaN
Product_MRP 1586.0 NaN NaN NaN 160.514054 30.359059 71.35 141.72 168.32 182.9375 226.59
Store_Id 1586 1 OUT001 1586 NaN NaN NaN NaN NaN NaN NaN
Store_Establishment_Year 1586.0 NaN NaN NaN 1987.0 0.0 1987.0 1987.0 1987.0 1987.0 1987.0
Store_Size 1586 1 High 1586 NaN NaN NaN NaN NaN NaN NaN
Store_Location_City_Type 1586 1 Tier 2 1586 NaN NaN NaN NaN NaN NaN NaN
Store_Type 1586 1 Supermarket Type1 1586 NaN NaN NaN NaN NaN NaN NaN
Product_Store_Sales_Total 1586.0 NaN NaN NaN 3923.778802 904.62901 2300.56 3285.51 4139.645 4639.4 4997.63
In [41]:
df_OUT001 = (
    data.loc[data['Store_Id'] == 'OUT001']
    .groupby(['Product_Type'], as_index=False)['Product_Store_Sales_Total']
    .sum()
)
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
plt.xlabel("Product_Type")
plt.ylabel("Product_Store_Sales_Total")
plt.title("Product_Store_Sales_Total")
sns.barplot(x=df_OUT001.Product_Type, y=df_OUT001.Product_Store_Sales_Total)
plt.show()
No description has been provided for this image
  • OUT001 has generated the highest revenue from sales of fruits and vegetables and Snack Foods. Both of categories contributed around 800,000 each.
In [42]:
# OUT002

data.loc[data['Store_Id'] == 'OUT002'].describe(include='all').T
Out[42]:
count unique top freq mean std min 25% 50% 75% max
Product_Id 1152 1152 NC2769 1 NaN NaN NaN NaN NaN NaN NaN
Product_Weight 1152.0 NaN NaN NaN 9.911241 1.799846 4.0 8.7675 9.795 10.89 19.82
Product_Sugar_Content 1152 4 Low Sugar 658 NaN NaN NaN NaN NaN NaN NaN
Product_Allocated_Area 1152.0 NaN NaN NaN 0.067747 0.047567 0.006 0.031 0.0545 0.09525 0.292
Product_Type 1152 16 Fruits and Vegetables 168 NaN NaN NaN NaN NaN NaN NaN
Product_MRP 1152.0 NaN NaN NaN 107.080634 24.912333 31.0 92.8275 104.675 117.8175 224.93
Store_Id 1152 1 OUT002 1152 NaN NaN NaN NaN NaN NaN NaN
Store_Establishment_Year 1152.0 NaN NaN NaN 1998.0 0.0 1998.0 1998.0 1998.0 1998.0 1998.0
Store_Size 1152 1 Small 1152 NaN NaN NaN NaN NaN NaN NaN
Store_Location_City_Type 1152 1 Tier 3 1152 NaN NaN NaN NaN NaN NaN NaN
Store_Type 1152 1 Food Mart 1152 NaN NaN NaN NaN NaN NaN NaN
Product_Store_Sales_Total 1152.0 NaN NaN NaN 1762.942465 462.862431 33.0 1495.4725 1889.495 2133.6225 2299.63
  1. OUT002 is a food mart which is located in a Tier 3 city and has store size as small. It was estabished in 1998.

  2. OUT002 has sold products whose MRP range from 31 to 225.

  3. Fruits and vegetables are the most sold product.

  4. The revenue generated from each product at OUT002 from 33 to 2300.

In [43]:
OUT00_df = (
    data.loc[data['Store_Id'] == 'OUT002']
    .groupby(['Product_Type'], as_index=False)['Product_Store_Sales_Total']
    .sum()
)
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
plt.xlabel("Product_Type")
plt.ylabel("Product_Store_Sales_Total")
plt.title("Product_Store_Sales_Total")
sns.barplot(x=OUT00_df.Product_Type, y=OUT00_df.Product_Store_Sales_Total)
plt.show()
No description has been provided for this image
  • OUT002 has generated the highest revenue from the sale of fruits and vegetables (~ 300,000) followed by snack foods (250,000).
In [44]:
# OUT003
data.loc[data['Store_Id'] == 'OUT003'].describe(include='all').T
Out[44]:
count unique top freq mean std min 25% 50% 75% max
Product_Id 1349 1349 NC522 1 NaN NaN NaN NaN NaN NaN NaN
Product_Weight 1349.0 NaN NaN NaN 15.103692 1.893531 7.35 14.02 15.18 16.35 22.0
Product_Sugar_Content 1349 4 Low Sugar 750 NaN NaN NaN NaN NaN NaN NaN
Product_Allocated_Area 1349.0 NaN NaN NaN 0.068637 0.048708 0.004 0.031 0.057 0.094 0.298
Product_Type 1349 16 Snack Foods 186 NaN NaN NaN NaN NaN NaN NaN
Product_MRP 1349.0 NaN NaN NaN 181.358725 24.796429 85.88 166.92 179.67 198.07 266.0
Store_Id 1349 1 OUT003 1349 NaN NaN NaN NaN NaN NaN NaN
Store_Establishment_Year 1349.0 NaN NaN NaN 1999.0 0.0 1999.0 1999.0 1999.0 1999.0 1999.0
Store_Size 1349 1 Medium 1349 NaN NaN NaN NaN NaN NaN NaN
Store_Location_City_Type 1349 1 Tier 1 1349 NaN NaN NaN NaN NaN NaN NaN
Store_Type 1349 1 Departmental Store 1349 NaN NaN NaN NaN NaN NaN NaN
Product_Store_Sales_Total 1349.0 NaN NaN NaN 4946.966323 677.539953 3069.24 4355.39 4958.29 5366.59 8000.0
  1. OUT003 is a Departmental store which is located in a Tier 1 city and has store as medium. It was established in 1999.
  2. OUT003 has sold products whose MRP range from 86 to 266.
  3. Snack Foods have been sold the highest number of times in OUT003.
  4. The revenue generated from each product at OUT003 ranges from 3070 to 8000.
In [45]:
OUT003_df = (
    data.loc[data['Store_Id'] == 'OUT003']
    .groupby(['Product_Type'], as_index=False)['Product_Store_Sales_Total']
    .sum()
)
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
plt.xlabel("Product_Type")
plt.ylabel("Product_Store_Sales_Total")
plt.title("Product_Store_Sales_Total")
sns.barplot(x=OUT003_df.Product_Type, y=OUT003_df.Product_Store_Sales_Total)
plt.show()
No description has been provided for this image
  • OUT003 has generated the highest revenue from the sale of snack foods folowed by fruits and vegetables, both the categories contributing around 800,000 each.
In [46]:
# OUT004
data.loc[data['Store_Id'] == 'OUT004'].describe(include='all').T
Out[46]:
count unique top freq mean std min 25% 50% 75% max
Product_Id 4676 4676 NC584 1 NaN NaN NaN NaN NaN NaN NaN
Product_Weight 4676.0 NaN NaN NaN 12.349613 1.428199 7.34 11.37 12.37 13.3025 17.79
Product_Sugar_Content 4676 4 Low Sugar 2632 NaN NaN NaN NaN NaN NaN NaN
Product_Allocated_Area 4676.0 NaN NaN NaN 0.069092 0.048584 0.004 0.031 0.056 0.097 0.297
Product_Type 4676 16 Fruits and Vegetables 700 NaN NaN NaN NaN NaN NaN NaN
Product_MRP 4676.0 NaN NaN NaN 142.399709 17.513973 83.04 130.54 142.82 154.1925 197.66
Store_Id 4676 1 OUT004 4676 NaN NaN NaN NaN NaN NaN NaN
Store_Establishment_Year 4676.0 NaN NaN NaN 2009.0 0.0 2009.0 2009.0 2009.0 2009.0 2009.0
Store_Size 4676 1 Medium 4676 NaN NaN NaN NaN NaN NaN NaN
Store_Location_City_Type 4676 1 Tier 2 4676 NaN NaN NaN NaN NaN NaN NaN
Store_Type 4676 1 Supermarket Type2 4676 NaN NaN NaN NaN NaN NaN NaN
Product_Store_Sales_Total 4676.0 NaN NaN NaN 3299.312111 468.271692 1561.06 2942.085 3304.18 3646.9075 5462.86
  • OUT004 is a store of Supermarket Type2 which is located in a Tier 2 city and has store size as medium. It was established in 2009.
  • OUT004 has sold products whose MRP range from 83 to 198.
  • Fruits and Vegetables have been sold the highest number of times in OUT004.
  • The revenue generated from each product at OUT004 ranges from 1561 to 5463.
In [47]:
OUT004_df = (
    data.loc[data['Store_Id'] == 'OUT004']
    .groupby(['Product_Type'], as_index=False)['Product_Store_Sales_Total']
    .sum()
)
plt.figure(figsize=[10, 8])
plt.xticks(rotation=90)
plt.xlabel("Product_Type")
plt.ylabel("Product_Store_Sales_Total")
plt.title("Product_Store_Sales_Total")
sns.barplot(x=OUT004_df.Product_Type, y=OUT004_df.Product_Store_Sales_Total)
plt.show()
No description has been provided for this image
  • OUT004 has generated the highest revenue from the sale of fruits and vegetables (~ 2500000) followed by snack foods (~2000000).
In [48]:
# revenue generated by the stores from each of the product types
df1 = data.groupby(['Store_Id', 'Product_Type'], as_index=False)[
    'Product_Store_Sales_Total'
    ].sum()
df1
Out[48]:
Store_Id Product_Type Product_Store_Sales_Total
0 OUT001 Baking Goods 525131.04
1 OUT001 Breads 121274.09
2 OUT001 Breakfast 38161.10
3 OUT001 Canned 449016.38
4 OUT001 Dairy 598767.62
5 OUT001 Frozen Foods 558556.81
6 OUT001 Fruits and Vegetables 792992.59
7 OUT001 Hard Drinks 152920.74
8 OUT001 Health and Hygiene 435005.31
9 OUT001 Household 531371.38
10 OUT001 Meat 505867.28
11 OUT001 Others 123977.09
12 OUT001 Seafood 52936.84
13 OUT001 Snack Foods 806142.24
14 OUT001 Soft Drinks 410548.69
15 OUT001 Starchy Foods 120443.98
16 OUT002 Baking Goods 169860.50
17 OUT002 Breads 43419.47
18 OUT002 Breakfast 23396.10
19 OUT002 Canned 151467.66
20 OUT002 Dairy 178888.18
21 OUT002 Frozen Foods 180295.95
22 OUT002 Fruits and Vegetables 298503.56
23 OUT002 Hard Drinks 54281.85
24 OUT002 Health and Hygiene 164660.81
25 OUT002 Household 184665.65
26 OUT002 Meat 151800.01
27 OUT002 Others 32835.73
28 OUT002 Seafood 17663.35
29 OUT002 Snack Foods 255317.57
30 OUT002 Soft Drinks 103808.35
31 OUT002 Starchy Foods 20044.98
32 OUT003 Baking Goods 491908.20
33 OUT003 Breads 175391.93
34 OUT003 Breakfast 95634.08
35 OUT003 Canned 452445.17
36 OUT003 Dairy 715814.94
37 OUT003 Frozen Foods 597608.42
38 OUT003 Fruits and Vegetables 897437.46
39 OUT003 Hard Drinks 110760.30
40 OUT003 Health and Hygiene 439139.18
41 OUT003 Household 523981.64
42 OUT003 Meat 520939.68
43 OUT003 Others 159963.75
44 OUT003 Seafood 65337.48
45 OUT003 Snack Foods 918510.44
46 OUT003 Soft Drinks 365046.30
47 OUT003 Starchy Foods 143538.60
48 OUT004 Baking Goods 1266086.26
49 OUT004 Breads 374856.75
50 OUT004 Breakfast 204939.13
51 OUT004 Canned 1247153.50
52 OUT004 Dairy 1318447.30
53 OUT004 Frozen Foods 1473519.65
54 OUT004 Fruits and Vegetables 2311899.66
55 OUT004 Hard Drinks 307851.73
56 OUT004 Health and Hygiene 1124901.91
57 OUT004 Household 1324721.50
58 OUT004 Meat 950604.97
59 OUT004 Others 224719.73
60 OUT004 Seafood 136466.37
61 OUT004 Snack Foods 2009026.70
62 OUT004 Soft Drinks 917641.38
63 OUT004 Starchy Foods 234746.89
  • In all the product types, the revenue generated by OUT004 has been the highest which seems quite logical since around 53% of the total products were brought from this store.
  • In all the product categories, the revenue generated by OUT002 has been the lowest which seems quite obvious since it is small store in a Tier 3 city.
In [49]:
# revenue generated by the stores from products having different levels of sugar content

df2 = data.groupby(['Store_Id', 'Product_Sugar_Content'], as_index=False)[
    'Product_Store_Sales_Total'
    ].sum()
df2
Out[49]:
Store_Id Product_Sugar_Content Product_Store_Sales_Total
0 OUT001 Low Sugar 3300834.93
1 OUT001 No Sugar 1090353.78
2 OUT001 Regular 1749444.51
3 OUT001 reg 82479.96
4 OUT002 Low Sugar 1156758.85
5 OUT002 No Sugar 382162.19
6 OUT002 Regular 472112.50
7 OUT002 reg 19876.18
8 OUT003 Low Sugar 3706903.24
9 OUT003 No Sugar 1123084.57
10 OUT003 Regular 1743566.35
11 OUT003 reg 99903.41
12 OUT004 Low Sugar 8658908.78
13 OUT004 No Sugar 2674343.14
14 OUT004 Regular 3902547.93
15 OUT004 reg 191783.58
  • The trend is the same as that which was present in the revenue analysis of stores for product types.

Data Preprocessing¶

In [50]:
# there is 3 types of sugar content in the Product_Sugar_Content column - Low Sugar, Regualr and reg
data.Product_Sugar_Content.replace(to_replace='reg', value='Regular', inplace=True)
In [51]:
data.Product_Sugar_Content.value_counts()
Out[51]:
count
Product_Sugar_Content
Low Sugar 4885
Regular 2359
No Sugar 1519

In [52]:
# Product_Id : 2 characters followed by a number
data["Product_Id_char"] = data["Product_Id"].str[:2]
data.head()
Out[52]:
Product_Id Product_Weight Product_Sugar_Content Product_Allocated_Area Product_Type Product_MRP Store_Id Store_Establishment_Year Store_Size Store_Location_City_Type Store_Type Product_Store_Sales_Total Product_Id_char
0 FD6114 12.66 Low Sugar 0.027 Frozen Foods 117.08 OUT004 2009 Medium Tier 2 Supermarket Type2 2842.40 FD
1 FD7839 16.54 Low Sugar 0.144 Dairy 171.43 OUT003 1999 Medium Tier 1 Departmental Store 4830.02 FD
2 FD5075 14.28 Regular 0.031 Canned 162.08 OUT001 1987 High Tier 2 Supermarket Type1 4130.16 FD
3 FD8233 12.10 Low Sugar 0.112 Baking Goods 186.31 OUT001 1987 High Tier 2 Supermarket Type1 4132.18 FD
4 NC1180 9.57 No Sugar 0.010 Health and Hygiene 123.67 OUT002 1998 Small Tier 3 Food Mart 2279.36 NC
In [53]:
data["Product_Id_char"].unique()
Out[53]:
array(['FD', 'NC', 'DR'], dtype=object)
In [54]:
data.loc[data.Product_Id_char == "FD", "Product_Type"].unique()
Out[54]:
array(['Frozen Foods', 'Dairy', 'Canned', 'Baking Goods', 'Snack Foods',
       'Meat', 'Fruits and Vegetables', 'Breads', 'Breakfast',
       'Starchy Foods', 'Seafood'], dtype=object)
In [55]:
data.loc[data.Product_Id_char == "NC", "Product_Type"].unique()
Out[55]:
array(['Health and Hygiene', 'Household', 'Others'], dtype=object)
In [56]:
data.loc[data.Product_Id_char == "DR", "Product_Type"].unique()
Out[56]:
array(['Hard Drinks', 'Soft Drinks'], dtype=object)
In [57]:
# store's age
data["Store_Age_Years"] = 2025 - data.Store_Establishment_Year
data.head()
Out[57]:
Product_Id Product_Weight Product_Sugar_Content Product_Allocated_Area Product_Type Product_MRP Store_Id Store_Establishment_Year Store_Size Store_Location_City_Type Store_Type Product_Store_Sales_Total Product_Id_char Store_Age_Years
0 FD6114 12.66 Low Sugar 0.027 Frozen Foods 117.08 OUT004 2009 Medium Tier 2 Supermarket Type2 2842.40 FD 16
1 FD7839 16.54 Low Sugar 0.144 Dairy 171.43 OUT003 1999 Medium Tier 1 Departmental Store 4830.02 FD 26
2 FD5075 14.28 Regular 0.031 Canned 162.08 OUT001 1987 High Tier 2 Supermarket Type1 4130.16 FD 38
3 FD8233 12.10 Low Sugar 0.112 Baking Goods 186.31 OUT001 1987 High Tier 2 Supermarket Type1 4132.18 FD 38
4 NC1180 9.57 No Sugar 0.010 Health and Hygiene 123.67 OUT002 1998 Small Tier 3 Food Mart 2279.36 NC 27
In [58]:
# grouping product types into perishables and non-perishables
perishables = [
    "Dairy",
    "Meat",
    "Fruits and Vegetables",
    "Breakfast",
    "Breads",
    "Seafood",
]
In [59]:
def change(x):
  if x in "Perishable":
    return "Perishables"
  else:
    return "Non-Perishables"
In [60]:
data['Product_Type_Category'] = data['Product_Type'].apply(change)
In [61]:
data.head()
Out[61]:
Product_Id Product_Weight Product_Sugar_Content Product_Allocated_Area Product_Type Product_MRP Store_Id Store_Establishment_Year Store_Size Store_Location_City_Type Store_Type Product_Store_Sales_Total Product_Id_char Store_Age_Years Product_Type_Category
0 FD6114 12.66 Low Sugar 0.027 Frozen Foods 117.08 OUT004 2009 Medium Tier 2 Supermarket Type2 2842.40 FD 16 Non-Perishables
1 FD7839 16.54 Low Sugar 0.144 Dairy 171.43 OUT003 1999 Medium Tier 1 Departmental Store 4830.02 FD 26 Non-Perishables
2 FD5075 14.28 Regular 0.031 Canned 162.08 OUT001 1987 High Tier 2 Supermarket Type1 4130.16 FD 38 Non-Perishables
3 FD8233 12.10 Low Sugar 0.112 Baking Goods 186.31 OUT001 1987 High Tier 2 Supermarket Type1 4132.18 FD 38 Non-Perishables
4 NC1180 9.57 No Sugar 0.010 Health and Hygiene 123.67 OUT002 1998 Small Tier 3 Food Mart 2279.36 NC 27 Non-Perishables
In [62]:
# Outlier Check
numeric_cols = data.select_dtypes(include=np.number).columns.tolist()
numeric_cols.remove("Store_Establishment_Year")
numeric_cols.remove("Store_Age_Years")

plt.figure(figsize=(16, 10))
for i, var in enumerate(numeric_cols):
    plt.subplot(3, 3, i + 1)
    plt.boxplot(data[var], whis=1.5)
    plt.tight_layout()
    plt.xlabel(var)
plt.show()
No description has been provided for this image
In [63]:
# data preprocessing pipeline for the categorical features
In [64]:
categorical_features = data.select_dtypes(include=["object", "category"]).columns.tolist()
categorical_features
Out[64]:
['Product_Id',
 'Product_Sugar_Content',
 'Product_Type',
 'Store_Id',
 'Store_Size',
 'Store_Location_City_Type',
 'Store_Type',
 'Product_Id_char',
 'Product_Type_Category']
In [65]:
preprocessor = make_column_transformer(
    (Pipeline([('encoder', OneHotEncoder(handle_unknown='ignore'))]), categorical_features)
)

Model Building¶

In [70]:
def adj_r2_score(predictors, targets, predictions):
    r2 = r2_score(targets, predictions)
    n = predictors.shape[0]
    k = predictors.shape[1]
    return 1 - ((1 - r2) * (n - 1) / (n - k - 1))

def model_performance_regression(model, predictors, target):
    """
    Function to compute different metrics to check regression model performance

    model: regressor
    predictors: independent variables
    target: dependent variable
    """

    # predicting using the independent variables
    pred = model.predict(predictors)

    r2 = r2_score(target, pred)  # to compute R-squared
    adjr2 = adj_r2_score(predictors, target, pred)  # to compute adjusted R-squared
    rmse = np.sqrt(mean_squared_error(target, pred))  # to compute RMSE
    mae = mean_absolute_error(target, pred)  # to compute MAE
    mape = mean_absolute_percentage_error(target, pred)

    df_perf = pd.DataFrame(
        {
            "RMSE": rmse,
            "MAE": mae,
            "R-squared": r2,
            "Adj. R-squared": adjr2,
            'MAPE': mape,
        },
        index=[0],
    )

    return df_perf
In [71]:
data = data.drop(["Product_Id","Product_Type","Store_Id","Store_Establishment_Year"], axis=1)
In [72]:
data.shape
Out[72]:
(8763, 11)
In [73]:
data.head()
Out[73]:
Product_Weight Product_Sugar_Content Product_Allocated_Area Product_MRP Store_Size Store_Location_City_Type Store_Type Product_Store_Sales_Total Product_Id_char Store_Age_Years Product_Type_Category
0 12.66 Low Sugar 0.027 117.08 Medium Tier 2 Supermarket Type2 2842.40 FD 16 Non-Perishables
1 16.54 Low Sugar 0.144 171.43 Medium Tier 1 Departmental Store 4830.02 FD 26 Non-Perishables
2 14.28 Regular 0.031 162.08 High Tier 2 Supermarket Type1 4130.16 FD 38 Non-Perishables
3 12.10 Low Sugar 0.112 186.31 High Tier 2 Supermarket Type1 4132.18 FD 38 Non-Perishables
4 9.57 No Sugar 0.010 123.67 Small Tier 3 Food Mart 2279.36 NC 27 Non-Perishables
In [75]:
X = data.drop("Product_Store_Sales_Total", axis=1)
y = data["Product_Store_Sales_Total"]
In [76]:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1, shuffle=True)
In [77]:
X_train.shape, X_test.shape
Out[77]:
((7010, 10), (1753, 10))
In [79]:
categorical_features = X_train.select_dtypes(include=["object", "category"]).columns.tolist()
categorical_features
Out[79]:
['Product_Sugar_Content',
 'Store_Size',
 'Store_Location_City_Type',
 'Store_Type',
 'Product_Id_char',
 'Product_Type_Category']
In [81]:
# run preprocessing pipeline for the categorical features
preprocessor = make_column_transformer(
    (
        OneHotEncoder(handle_unknown="ignore", sparse_output=False),
        categorical_features,
    ),
    remainder="passthrough",
)

Define functions for Model Evaluation¶

In [82]:
# function to compute adjusted R-squared
def adj_r2_score(predictors, targets, predictions):
    r2 = r2_score(targets, predictions)
    n = predictors.shape[0]
    k = predictors.shape[1]
    return 1 - ((1 - r2) * (n - 1) / (n - k - 1))


# function to compute different metrics to check performance of a regression model
def model_performance_regression(model, predictors, target):
    """
    Function to compute different metrics to check regression model performance

    model: regressor
    predictors: independent variables
    target: dependent variable
    """

    # predicting using the independent variables
    pred = model.predict(predictors)

    r2 = r2_score(target, pred)  # to compute R-squared
    adjr2 = adj_r2_score(predictors, target, pred)  # to compute adjusted R-squared
    rmse = np.sqrt(mean_squared_error(target, pred))  # to compute RMSE
    mae = mean_absolute_error(target, pred)  # to compute MAE
    mape = mean_absolute_percentage_error(target, pred)  # to compute MAPE

    # creating a dataframe of metrics
    df_perf = pd.DataFrame(
        {
            "RMSE": rmse,
            "MAE": mae,
            "R-squared": r2,
            "Adj. R-squared": adjr2,
            "MAPE": mape,
        },
        index=[0],
    )

    return df_perf

The ML models to be built can be any two out of the following:

  1. Decision Tree
  2. Bagging
  3. Random Forest
  4. AdaBoost
  5. Gradient Boosting
  6. XGBoost

Model Performance Improvement - Hyperparameter Tuning¶

AdaBoost Regressor

In [84]:
# classifier
ab_tuning = AdaBoostRegressor(random_state=1)
ab_tuning = make_pipeline(preprocessor, ab_tuning)

# grid of paramters to choose
parameters = {
    "adaboostregressor__n_estimators": [50, 100],
    "adaboostregressor__learning_rate": [0.1, 1.0],
}

# run the grid search
grid_obj = GridSearchCV(ab_tuning, parameters, cv=5, n_jobs=-1)
grid_obj = grid_obj.fit(X_train, y_train)

# set clf
ab_tuning = grid_obj.best_estimator_

# fit the data
ab_tuning.fit(X_train, y_train)
Out[84]:
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('onehotencoder',
                                                  OneHotEncoder(handle_unknown='ignore',
                                                                sparse_output=False),
                                                  ['Product_Sugar_Content',
                                                   'Store_Size',
                                                   'Store_Location_City_Type',
                                                   'Store_Type',
                                                   'Product_Id_char',
                                                   'Product_Type_Category'])])),
                ('adaboostregressor',
                 AdaBoostRegressor(learning_rate=0.1, random_state=1))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('onehotencoder',
                                                  OneHotEncoder(handle_unknown='ignore',
                                                                sparse_output=False),
                                                  ['Product_Sugar_Content',
                                                   'Store_Size',
                                                   'Store_Location_City_Type',
                                                   'Store_Type',
                                                   'Product_Id_char',
                                                   'Product_Type_Category'])])),
                ('adaboostregressor',
                 AdaBoostRegressor(learning_rate=0.1, random_state=1))])
ColumnTransformer(remainder='passthrough',
                  transformers=[('onehotencoder',
                                 OneHotEncoder(handle_unknown='ignore',
                                               sparse_output=False),
                                 ['Product_Sugar_Content', 'Store_Size',
                                  'Store_Location_City_Type', 'Store_Type',
                                  'Product_Id_char',
                                  'Product_Type_Category'])])
['Product_Sugar_Content', 'Store_Size', 'Store_Location_City_Type', 'Store_Type', 'Product_Id_char', 'Product_Type_Category']
OneHotEncoder(handle_unknown='ignore', sparse_output=False)
['Product_Weight', 'Product_Allocated_Area', 'Product_MRP', 'Store_Age_Years']
passthrough
AdaBoostRegressor(learning_rate=0.1, random_state=1)
In [85]:
# model performance training set
ab_tuning_perf = model_performance_regression(ab_tuning, X_test, y_test)
ab_tuning_perf
Out[85]:
RMSE MAE R-squared Adj. R-squared MAPE
0 471.692134 360.34973 0.806985 0.805877 0.121427
In [86]:
# model performance test set
ab_tuning_perf = model_performance_regression(ab_tuning, X_test, y_test)
ab_tuning_perf
Out[86]:
RMSE MAE R-squared Adj. R-squared MAPE
0 471.692134 360.34973 0.806985 0.805877 0.121427

Gradient Boosting Regressor

In [87]:
# select classifier
gb_tuning = GradientBoostingRegressor(random_state=1)
gb_tuning = make_pipeline(preprocessor, gb_tuning)

# grid of paramters to choose
parameters = {
    "gradientboostingregressor__n_estimators": [50, 100],
    "gradientboostingregressor__subsample": [0.1],
    "gradientboostingregressor__max_features": [3, 5],
    "gradientboostingregressor__max_depth": [2, 4],
}

# run the grid search
grid_obj = GridSearchCV(gb_tuning, parameters, cv=5, n_jobs=-1)
grid_obj = grid_obj.fit(X_train, y_train)

# set clf
gb_tuning = grid_obj.best_estimator_

# fit into data
gb_tuning.fit(X_train, y_train)
Out[87]:
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('onehotencoder',
                                                  OneHotEncoder(handle_unknown='ignore',
                                                                sparse_output=False),
                                                  ['Product_Sugar_Content',
                                                   'Store_Size',
                                                   'Store_Location_City_Type',
                                                   'Store_Type',
                                                   'Product_Id_char',
                                                   'Product_Type_Category'])])),
                ('gradientboostingregressor',
                 GradientBoostingRegressor(max_depth=4, max_features=5,
                                           random_state=1, subsample=0.1))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('onehotencoder',
                                                  OneHotEncoder(handle_unknown='ignore',
                                                                sparse_output=False),
                                                  ['Product_Sugar_Content',
                                                   'Store_Size',
                                                   'Store_Location_City_Type',
                                                   'Store_Type',
                                                   'Product_Id_char',
                                                   'Product_Type_Category'])])),
                ('gradientboostingregressor',
                 GradientBoostingRegressor(max_depth=4, max_features=5,
                                           random_state=1, subsample=0.1))])
ColumnTransformer(remainder='passthrough',
                  transformers=[('onehotencoder',
                                 OneHotEncoder(handle_unknown='ignore',
                                               sparse_output=False),
                                 ['Product_Sugar_Content', 'Store_Size',
                                  'Store_Location_City_Type', 'Store_Type',
                                  'Product_Id_char',
                                  'Product_Type_Category'])])
['Product_Sugar_Content', 'Store_Size', 'Store_Location_City_Type', 'Store_Type', 'Product_Id_char', 'Product_Type_Category']
OneHotEncoder(handle_unknown='ignore', sparse_output=False)
['Product_Weight', 'Product_Allocated_Area', 'Product_MRP', 'Store_Age_Years']
passthrough
GradientBoostingRegressor(max_depth=4, max_features=5, random_state=1,
                          subsample=0.1)
In [88]:
# model performance for training set
gb_tuning_perf = model_performance_regression(gb_tuning, X_train, y_train)
gb_tuning_perf
Out[88]:
RMSE MAE R-squared Adj. R-squared MAPE
0 306.261446 160.801822 0.917047 0.916928 0.060211
In [89]:
# model performance for test set
gb_tuning_perf = model_performance_regression(gb_tuning, X_test, y_test)
gb_tuning_perf
Out[89]:
RMSE MAE R-squared Adj. R-squared MAPE
0 329.170871 174.151943 0.906002 0.905463 0.061328

XGBoost Regressor

In [90]:
# classifer
xgb_tuning = XGBRegressor(random_state=1)
xgb_tuning = make_pipeline(preprocessor, xgb_tuning)

# grid of parameters to choose
parameters = {
    "xgbregressor__n_estimators": [50, 100],
    "xgbregressor__subsample": [0.1],
    "xgbregressor__gamma": [0.1],
    "xgbregressor__colsample_bytree": [0.5],
    "xgbregressor__colsample_bylevel": [0.5],
}

# grid search run
grid_obj = GridSearchCV(xgb_tuning, parameters, cv=5, n_jobs=-1)
grid_obj = grid_obj.fit(X_train, y_train)

# set clf
xgb_tuning = grid_obj.best_estimator_

# fit into data
xgb_tuning.fit(X_train, y_train)
Out[90]:
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('onehotencoder',
                                                  OneHotEncoder(handle_unknown='ignore',
                                                                sparse_output=False),
                                                  ['Product_Sugar_Content',
                                                   'Store_Size',
                                                   'Store_Location_City_Type',
                                                   'Store_Type',
                                                   'Product_Id_char',
                                                   'Product_Type_Category'])])),
                ('xgbregressor',
                 XGBRegressor(base_score=None, booster=Non...
                              feature_types=None, gamma=0.1, grow_policy=None,
                              importance_type=None,
                              interaction_constraints=None, learning_rate=None,
                              max_bin=None, max_cat_threshold=None,
                              max_cat_to_onehot=None, max_delta_step=None,
                              max_depth=None, max_leaves=None,
                              min_child_weight=None, missing=nan,
                              monotone_constraints=None, multi_strategy=None,
                              n_estimators=50, n_jobs=None,
                              num_parallel_tree=None, random_state=1, ...))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('onehotencoder',
                                                  OneHotEncoder(handle_unknown='ignore',
                                                                sparse_output=False),
                                                  ['Product_Sugar_Content',
                                                   'Store_Size',
                                                   'Store_Location_City_Type',
                                                   'Store_Type',
                                                   'Product_Id_char',
                                                   'Product_Type_Category'])])),
                ('xgbregressor',
                 XGBRegressor(base_score=None, booster=Non...
                              feature_types=None, gamma=0.1, grow_policy=None,
                              importance_type=None,
                              interaction_constraints=None, learning_rate=None,
                              max_bin=None, max_cat_threshold=None,
                              max_cat_to_onehot=None, max_delta_step=None,
                              max_depth=None, max_leaves=None,
                              min_child_weight=None, missing=nan,
                              monotone_constraints=None, multi_strategy=None,
                              n_estimators=50, n_jobs=None,
                              num_parallel_tree=None, random_state=1, ...))])
ColumnTransformer(remainder='passthrough',
                  transformers=[('onehotencoder',
                                 OneHotEncoder(handle_unknown='ignore',
                                               sparse_output=False),
                                 ['Product_Sugar_Content', 'Store_Size',
                                  'Store_Location_City_Type', 'Store_Type',
                                  'Product_Id_char',
                                  'Product_Type_Category'])])
['Product_Sugar_Content', 'Store_Size', 'Store_Location_City_Type', 'Store_Type', 'Product_Id_char', 'Product_Type_Category']
OneHotEncoder(handle_unknown='ignore', sparse_output=False)
['Product_Weight', 'Product_Allocated_Area', 'Product_MRP', 'Store_Age_Years']
passthrough
XGBRegressor(base_score=None, booster=None, callbacks=None,
             colsample_bylevel=0.5, colsample_bynode=None, colsample_bytree=0.5,
             device=None, early_stopping_rounds=None, enable_categorical=False,
             eval_metric=None, feature_types=None, gamma=0.1, grow_policy=None,
             importance_type=None, interaction_constraints=None,
             learning_rate=None, max_bin=None, max_cat_threshold=None,
             max_cat_to_onehot=None, max_delta_step=None, max_depth=None,
             max_leaves=None, min_child_weight=None, missing=nan,
             monotone_constraints=None, multi_strategy=None, n_estimators=50,
             n_jobs=None, num_parallel_tree=None, random_state=1, ...)
In [91]:
# training set model
xgb_tuning_perf = model_performance_regression(xgb_tuning, X_train, y_train)
xgb_tuning_perf
Out[91]:
RMSE MAE R-squared Adj. R-squared MAPE
0 314.032694 175.464401 0.912784 0.912659 0.066634
In [92]:
# test set model
xgb_tuning_perf = model_performance_regression(xgb_tuning, X_test, y_test)
xgb_tuning_perf
Out[92]:
RMSE MAE R-squared Adj. R-squared MAPE
0 356.027652 192.660011 0.890038 0.889407 0.066096
In [93]:
# combine models
# comparing all train models

all_models_train_df = pd.concat(
    [
        ab_tuning_perf.T,
        gb_tuning_perf.T,
        xgb_tuning_perf.T,
    ],
    axis=1,
)

all_models_train_df.columns = ["AdaBoost", "Gradient Boosting", "XGBoost"]
all_models_train_df
Out[93]:
AdaBoost Gradient Boosting XGBoost
RMSE 471.692134 329.170871 356.027652
MAE 360.349730 174.151943 192.660011
R-squared 0.806985 0.906002 0.890038
Adj. R-squared 0.805877 0.905463 0.889407
MAPE 0.121427 0.061328 0.066096
In [100]:
# select location of the folder before deployment
os.makedirs("model_files", exist_ok=True)
In [101]:
# Define the file saved location
saved_model_location = "model_files/xgb_model.joblib"
In [114]:
# save the best model pipeline using joblib
joblib.dump(xgb_tuning, saved_model_location)
print(f"Model saved at {saved_model_location}")
Model saved at model_files/xgb_model.joblib
In [115]:
# load the saved model pipeline from the file
saved_model_location = "model_files/xgb_model.joblib"
saved_model = joblib.load(saved_model_location)
print("Model loaded successfully")
Model loaded successfully
In [116]:
saved_model
Out[116]:
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('onehotencoder',
                                                  OneHotEncoder(handle_unknown='ignore',
                                                                sparse_output=False),
                                                  ['Product_Sugar_Content',
                                                   'Store_Size',
                                                   'Store_Location_City_Type',
                                                   'Store_Type',
                                                   'Product_Id_char',
                                                   'Product_Type_Category'])])),
                ('xgbregressor',
                 XGBRegressor(base_score=None, booster=Non...
                              feature_types=None, gamma=0.1, grow_policy=None,
                              importance_type=None,
                              interaction_constraints=None, learning_rate=None,
                              max_bin=None, max_cat_threshold=None,
                              max_cat_to_onehot=None, max_delta_step=None,
                              max_depth=None, max_leaves=None,
                              min_child_weight=None, missing=nan,
                              monotone_constraints=None, multi_strategy=None,
                              n_estimators=50, n_jobs=None,
                              num_parallel_tree=None, random_state=1, ...))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('columntransformer',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('onehotencoder',
                                                  OneHotEncoder(handle_unknown='ignore',
                                                                sparse_output=False),
                                                  ['Product_Sugar_Content',
                                                   'Store_Size',
                                                   'Store_Location_City_Type',
                                                   'Store_Type',
                                                   'Product_Id_char',
                                                   'Product_Type_Category'])])),
                ('xgbregressor',
                 XGBRegressor(base_score=None, booster=Non...
                              feature_types=None, gamma=0.1, grow_policy=None,
                              importance_type=None,
                              interaction_constraints=None, learning_rate=None,
                              max_bin=None, max_cat_threshold=None,
                              max_cat_to_onehot=None, max_delta_step=None,
                              max_depth=None, max_leaves=None,
                              min_child_weight=None, missing=nan,
                              monotone_constraints=None, multi_strategy=None,
                              n_estimators=50, n_jobs=None,
                              num_parallel_tree=None, random_state=1, ...))])
ColumnTransformer(remainder='passthrough',
                  transformers=[('onehotencoder',
                                 OneHotEncoder(handle_unknown='ignore',
                                               sparse_output=False),
                                 ['Product_Sugar_Content', 'Store_Size',
                                  'Store_Location_City_Type', 'Store_Type',
                                  'Product_Id_char',
                                  'Product_Type_Category'])])
['Product_Sugar_Content', 'Store_Size', 'Store_Location_City_Type', 'Store_Type', 'Product_Id_char', 'Product_Type_Category']
OneHotEncoder(handle_unknown='ignore', sparse_output=False)
['Product_Weight', 'Product_Allocated_Area', 'Product_MRP', 'Store_Age_Years']
passthrough
XGBRegressor(base_score=None, booster=None, callbacks=None,
             colsample_bylevel=0.5, colsample_bynode=None, colsample_bytree=0.5,
             device=None, early_stopping_rounds=None, enable_categorical=False,
             eval_metric=None, feature_types=None, gamma=0.1, grow_policy=None,
             importance_type=None, interaction_constraints=None,
             learning_rate=None, max_bin=None, max_cat_threshold=None,
             max_cat_to_onehot=None, max_delta_step=None, max_depth=None,
             max_leaves=None, min_child_weight=None, missing=nan,
             monotone_constraints=None, multi_strategy=None, n_estimators=50,
             n_jobs=None, num_parallel_tree=None, random_state=1, ...)
In [117]:
saved_model.predict(X_test)
Out[117]:
array([3664.202 , 5927.408 , 4476.766 , ..., 4876.1636, 3134.6177,
       3540.1023], dtype=float32)

Deployment - Backend¶

Flask Web Framework¶

In [119]:
%%writefile model_files/app.py

import numpy as np
import joblib
import pandas as pd
from flask import Flask, request, jsonify

superkart_api = Flask(__name__)

model = joblib.load("model_files/xgb_model.joblib")

@superkart_api.get('/')
def home():
    return "Welcome to SuperKart"

@superkart_api.post('/v1/predict')
def sales_prediction():
    data = request.get_json()
    sample = {
        "Store_Id": data["Store_Id"],
        "Product_Id": data["Product_Id"],
        "Product_Sugar_Content": data["Product_Sugar_Content"],
        "Product_Type": data["Product_Type"],
        "Product_Weight": data["Product_Weight"],
        "Product_MRP": data["Product_MRP"],
        "Store_Location_Type": data["Store_Location_Type"],
        "Store_Type": data["Store_Type"],
        "Store_Size": data["Store_Size"],
        "Store_Age_Years": data["Store_Age_Years"],
        "Product_Type_Category": data["Product_Type_Category"],
    }

    import_data = pd.DataFrame([sample])
    prediction = model.predict(input_data).tolist()[0]
    return jsonify({'Sales': prediction})

if __name__ == '__main__':
    superkart_api.run(debug=True)
Writing model_files/app.py

Dependencies File¶

In [120]:
%%writefile model_files/required.txt
pandas==2.2.2
numpy==2.0.2
scikit-learn==1.6.1
seaborn==0.13.2
joblib==1.4.2
xgboost==2.1.4
joblib==1.4.2
Werkzeug==2.2.2
flask==2.2.2
gunicorn==20.1.0
requests==2.32.3
uvicorn[standard]
streamlit==1.43.2
Writing model_files/required.txt

Dockerfile¶

In [121]:
%%writefile model_files/Dockerfile
FROM python:3.9-slim

WORKDIR /app

COPY . .

RUN pip3 install -r requirements.txt

CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:7860", "app:superkart_api"]
Writing model_files/Dockerfile

Setting up a Hugging Face Docker Space for the Backend¶

In [125]:
from huggingface_hub import login
from google.colab import userdata

# Assuming your Hugging Face token is stored as a secret named 'HF_TOKEN'
HF_TOKEN = userdata.get('HF_TOKEN')
login(token=HF_TOKEN)

from huggingface_hub import create_repo
In [131]:
try:
  create_repo("superkart-api",
              repo_type="space",
              space_sdk="docker",
              private=False
  )
except Exception as e:
  if "Repository already exists" in str(e):
    print("Repo already exists")
  else:
    print(e)
    print("Error creating repo")

Uploading Files to Hugging Face Space (Docker Space)¶

In [138]:
access_key = userdata.get('HF_TOKEN')
repo_id = "airesoli/superkart-api"

login(token=access_key)
api = HfApi()

api.upload_folder(
    folder_path="model_files",
    repo_id=repo_id,
    path_in_repo="model_files",
    repo_type="space",
)
Processing Files (0 / 0)                : |          |  0.00B /  0.00B            
New Data Upload                         : |          |  0.00B /  0.00B            
  /content/model_files/xgb_model.joblib : 100%|##########|  143kB /  143kB            
Out[138]:
CommitInfo(commit_url='https://huggingface.co/spaces/airesoli/superkart-api/commit/f164c27ed230fa288b448d5ab899cff79628444a', commit_message='Upload folder using huggingface_hub', commit_description='', oid='f164c27ed230fa288b448d5ab899cff79628444a', pr_url=None, repo_url=RepoUrl('https://huggingface.co/spaces/airesoli/superkart-api', endpoint='https://huggingface.co', repo_type='space', repo_id='airesoli/superkart-api'), pr_revision=None, pr_num=None)

Deployment - Frontend¶

Points to note before executing the below cells¶

  • Create a Streamlit space on Hugging Face by following the instructions provided on the content page titled Creating Spaces and Adding Secrets in Hugging Face from Week 1

Streamlit for Interactive UI¶

In [139]:
# make a front end file at the same location with backend file
os.makedirs("frontend_files", exist_ok=True)
In [140]:
%%writefile frontend_files/app.py
import streamlit as st
import requests

st.title("SuperKart Sales Prediction")

Product_Weight = st.number_input("Product Weight")
Product_Sugar_Content = st.number_input("Product Sugar Content")
Product_MRP = st.number_input("Product MRP")
Store_Age_Years = st.number_input("Store Age Years")
Product_Allocated_Area = st.selectbox("Product Allocated Area", ["Small", "Medium", "Large"])
Store_Location_Type = st.selectbox("Store Location Type", ["Tier 1", "Tier 2", "Tier 3"])
Store_Type = st.selectbox("Store Type", ["Supermarket", "Grocery Store"])
Store_Size = st.selectbox("Store Size", ["Small", "Medium", "Large"])
Product_Type = st.selectbox("Product Type", ["Dairy", "Bakery", "Fruits and Vegetables"])
Store_Type_Category = st.selectbox("Store Type Category", ["Supermarket", "Grocery Store"])
Product_ID = st.text_input("Product ID")
Store_ID = st.text_input("Store ID")
Store_Age_Years = st.number_input("Store Age Years")
Product_Type_Category = st.selectbox("Product Type Category", ["Dairy", "Bakery", "Fruits and Vegetables"])

product_area = {
    "Product": Product_Allocated_Area,
    "Store": Store_Size,
    "Product_Type": Product_Type,
    "Store_Type": Store_Type,
    "Store_Location_Type": Store_Location_Type,
    "Product_Sugar_Content": Product,
    "Product_Weight": Product_Weight,
    "Product_MRP": Product_MRP,
    "Store_Age_Years": Store_Age_Years,
    "Product_Type_Category": Product_Type_Category,
    "Store_Type_Category": Store_Type_Category,
}

if st.button("Predict"):
    response = requests.post("https://airesoli-superkart-api.hf.space/v1/predict", json=product_area)
    if response.status_code == 200:
        result = response.json()
        st.success(f"Predicted Sales: {result['Sales']}")
    else:
        st.error("Error in prediction")
Writing frontend_files/app.py

Dependencies File¶

In [141]:
%%writefile frontend_files/requirements.txt
streamlit==1.45.0
requests==2.32.3
Writing frontend_files/requirements.txt

DockerFile¶

In [142]:
%%writefile frontend_files/Dockerfile
# Use a minimal base image with Python 3.9 installed
FROM python:3.9-slim

# Set the working directory inside the container to /app
WORKDIR /app

# Copy all files from the current directory on the host to the container's /app directory
COPY . .

# Install Python dependencies listed in requirements.txt
RUN pip3 install -r requirements.txt

# Define the command to run the Streamlit app on port 8501 and make it accessible externally
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.enableXsrfProtection=false"]

# NOTE: Disable XSRF protection for easier external access in order to make batch predictions
Writing frontend_files/Dockerfile

Uploading Files to Hugging Face Space (Streamlit Space)¶

In [143]:
access_key = userdata.get('HF_TOKEN')
repo_id = "airesoli/superkart-api"

login(token=access_key)

api = HfApi()

api.upload_folder(
    folder_path="frontend_files",
    repo_id=repo_id,
    path_in_repo="frontend_files",
    repo_type="space",
)
Out[143]:
CommitInfo(commit_url='https://huggingface.co/spaces/airesoli/superkart-api/commit/b58637ff8f422d2fe468883c72a962c7fad7d2a3', commit_message='Upload folder using huggingface_hub', commit_description='', oid='b58637ff8f422d2fe468883c72a962c7fad7d2a3', pr_url=None, repo_url=RepoUrl('https://huggingface.co/spaces/airesoli/superkart-api', endpoint='https://huggingface.co', repo_type='space', repo_id='airesoli/superkart-api'), pr_revision=None, pr_num=None)

Actionable Insights and Business Recommendations¶

  1. Goodness of Fit (R^2 and Adjusted R^2)R^2 (R-Squared): 0.8069

    • The model explains 80.7% of the variance in the data.
    • Since this value is close to 1.0, it indicates a strong fit and suggests the model accurately captures the underlying patterns.
    • Adjusted R^2: No Significant Change
    • The stability between R^2 and Adjusted $R^2$ indicates that the variables included are meaningful and the model is not being penalized for unnecessary complexity.
  2. Error MetricsMAPE (Mean Absolute Percentage Error): 12.1%On average, the model's predictions deviate from the actual values by 12.1%.

    • This represents the average "prediction error" (which may stem from data noise or human recording errors).
    • MAE (Mean Absolute Error): 360.35
    • The average absolute difference between the predicted and actual values is approximately 360.35 units.
    • RMSE (Root Mean Square Error): 471.69
    • The RMSE is higher than the MAE ($471.69 > 360.35$), which confirms the presence of outliers.
    • RMSE penalizes larger errors more heavily, indicating that a few data points have significantly higher variances than the rest.
  • Executive SummaryOverall, the model is highly accurate and precise, successfully explaining over 80% of the data's behavior.
  • However, the gap between the MAE and RMSE suggests that certain outliers are skewing the results.
  1. Recommendations: Outlier Investigation:
    • Identify the specific data points causing the higher RMSE to determine if they are valid anomalies or errors.
    • Feature Analysis: Review the input variables to identify which factors contribute most to the variance and see if additional data could further reduce the 12.1% error margin.
In [159]:
# 1. Save your notebook first (Ctrl + S)
from google.colab import files
import nbformat

file_path = '/content/drive/MyDrive/Colab Notebooks/'
save_path = '/content/drive/MyDrive/'

# 2. Get the name of your notebook
# (If you renamed it, change 'Untitled.ipynb' to your filename)
notebook_path = file_path + 'fullcode_superkart_erika.ipynb'

# 3. Clean the metadata that causes the 'state' error
with open(notebook_path, 'r', encoding='utf-8') as f:
    nb = nbformat.read(f, as_version=4)

# Remove problematic widget state metadata
if 'widgets' in nb.metadata:
    del nb.metadata['widgets']

# Save the "fixed" version temporarily
fixed_name = save_path + 'fullcode_superkart_erika.ipynb'
with open(fixed_name, 'w', encoding='utf-8') as f: # Changed save_path to fixed_name here
    nbformat.write(nb, f)

# 4. Convert the fixed file to HTML
!jupyter nbconvert --to html "{fixed_name}"

# 5. Download the final HTML file
files.download(fixed_name.replace('.ipynb', '.html'))
[NbConvertApp] Converting notebook /content/drive/MyDrive/fullcode_superkart_erika.ipynb to html
[NbConvertApp] WARNING | Alternative text is missing on 31 image(s).
[NbConvertApp] Writing 2684959 bytes to /content/drive/MyDrive/fullcode_superkart_erika.html