import pandas as pd
import numpy as np
import plotnine as g
from datetime import timedelta
Effective inventory management is essential in maintaining optimal levels of inventory which ensures that the right items are always available when needed while reducing the cost of holding inventory and ultimately avoiding stockouts or excess inventory.
Inventory classification using the ABC-XYZ analysis is a widely used technique which categorizes items based on their sales volume and demand variability, allowing businesses to have better control over thier inventories and to also use optimal strategies on items that are most important to their business. We will utilize the powerful framework of ABC-XYZ inventory classification model to analyze transaction data for products. By performing this analysis, we will be able to extract valuable insights and identify trends that can help inform strategic decisions around inventory management.
Load the packages
For this project the two major package we will use is the pandas package
for data summarization and reshaping and plotnine
for data visualization based on the grammer of graphics.
Code
def custom_theme(figure_size=(10, 4)):
return (
+
g.theme_minimal() =figure_size,
g.theme(figure_size=g.element_rect(fill="#FFFFFF", color="#FFFFFF"),
plot_background=g.element_rect(fill="#FFFFFF"),
panel_background=g.element_text(color="#545454", size=14),
plot_title=g.element_text(color="#4F4F4F", size=11),
axis_title=g.element_text(color="#474747"))
axis_text )
Import transaction data
= "https://raw.githubusercontent.com/akins11/SCM/main/posts/abc-xyz-analysis/data/scanner_data.csv"
path = pd.read_csv(path)
df
df.head()
Unnamed: 0 | Date | Customer_ID | Transaction_ID | SKU_Category | SKU | Quantity | Sales_Amount | |
---|---|---|---|---|---|---|---|---|
0 | 1 | 2017-01-02 | 2547 | 1 | X52 | 0EM7L | 1.0 | 3.13 |
1 | 2 | 2017-01-02 | 822 | 2 | 2ML | 68BRQ | 1.0 | 5.46 |
2 | 3 | 2017-01-02 | 3686 | 3 | 0H2 | CZUZX | 1.0 | 6.35 |
3 | 4 | 2017-01-02 | 3719 | 4 | 0H2 | 549KK | 1.0 | 5.59 |
4 | 5 | 2017-01-02 | 9200 | 5 | 0H2 | K8EHH | 1.0 | 6.88 |
Quick data inspection
This inspection is to help us identify any issue pertaining to the overall data.
Data struture
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 131706 entries, 0 to 131705
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Unnamed: 0 131706 non-null int64
1 Date 131706 non-null object
2 Customer_ID 131706 non-null int64
3 Transaction_ID 131706 non-null int64
4 SKU_Category 131706 non-null object
5 SKU 131706 non-null object
6 Quantity 131706 non-null float64
7 Sales_Amount 131706 non-null float64
dtypes: float64(2), int64(3), object(3)
memory usage: 8.0+ MB
There are 131,706 rows and 6 columns in total. The Date
variable have an object data type instead of datetime data type, also we will drop the Unnamed: 0
column which is not useful for this analysis.
SKU summary
Product SKU
which stands for stock keeping unit
is a unique identifier code assigned to each item in a company’s inventory and they are used to identify, track and manage inventories. We can check the total number of unique SKU by using the nunique()
method from pandas DataFrame.
print(f"There are {df['SKU'].nunique() :,} unique SKUs.")
There are 5,242 unique SKUs.
Quantity & sales summary
A descriptive summary of Quantity
which is the amount of a particular product customers are willing to purchase, and it appears that majority of the orders placed are for a single unit we can also see a major outlier of 400 representing the highest order.
"Quantity", "Sales_Amount"]].describe() df[[
Quantity | Sales_Amount | |
---|---|---|
count | 131706.000000 | 131706.000000 |
mean | 1.485311 | 11.981524 |
std | 3.872667 | 19.359699 |
min | 0.010000 | 0.020000 |
25% | 1.000000 | 4.230000 |
50% | 1.000000 | 6.920000 |
75% | 1.000000 | 12.330000 |
max | 400.000000 | 707.730000 |
Data cleaning
We will perform some data cleaning such as:
Remove
Unnamed: 0
andSKU_Category
columns which are not useful for this analysis.
Changing the
Date
variable to a datetime data type using the pandasto_datetime()
function.
And finally convert all column names to lower case.
= (
df
df"Unnamed: 0", "SKU_Category"], axis=1)
.drop([= lambda d: pd.to_datetime(d["Date"]))
.assign(Date
)
= df.columns.str.lower()
df.columns
df.head()
date | customer_id | transaction_id | sku | quantity | sales_amount | |
---|---|---|---|---|---|---|
0 | 2017-01-02 | 2547 | 1 | 0EM7L | 1.0 | 3.13 |
1 | 2017-01-02 | 822 | 2 | 68BRQ | 1.0 | 5.46 |
2 | 2017-01-02 | 3686 | 3 | CZUZX | 1.0 | 6.35 |
3 | 2017-01-02 | 3719 | 4 | 549KK | 1.0 | 5.59 |
4 | 2017-01-02 | 9200 | 5 | K8EHH | 1.0 | 6.88 |
Product orders for a 12-month period
To accurately determine the demand and variability for each product, we will use a 12-month period which provides sufficient amount of data for the analysis and also a representative sample of the patterns and trends of demand over time. To do this we will filter orders made in 2017 alone.
= (
df_m
df"date >= '2017-01-01' & date <= '2017-12-31'")
.query(= lambda d: d["date"].dt.month)
.assign(month )
ABC inventory classification
ABC inventory classification helps business group items based on demand, cost, revenue and the overall relative importance of the item to the business. It helps them realize which of their products or services have the most significant impact on their financial success.
It categories items into three separate classes where:
Items in
class A
are considared to have high demand volume and revenue, majorly80%
overall.
Items in
class B
have slightly moderate demand volume and revenue, considared to be15%
of the overall total.
Items in
class C
have reletively low demand volume and revenue.
To group each product, we will perform the following operations:
Group the transaction data by SKU and extract the number of times each product was purchased, the total demand for the product and the sum of all sales amount.
The resulting data from the previous step will be arranged from the highest revenue to the lowest revenue (e.i in descending order).
= (
abc_df
df_m"sku")
.groupby(= ("sku", "nunique"),
.agg(unique_purchases = ("quantity", "sum"),
total_demand = ("sales_amount", "sum"))
total_revenue ="total_revenue", ascending=False)
.sort_values(by
.reset_index()
)
abc_df.head()
sku | unique_purchases | total_demand | total_revenue | |
---|---|---|---|---|
0 | 3YDVF | 1 | 926.0 | 29419.35 |
1 | LJ26I | 1 | 632.0 | 13571.45 |
2 | IRQZ4 | 1 | 321.0 | 11681.63 |
3 | DWJC4 | 1 | 823.0 | 10592.09 |
4 | 44RUE | 1 | 218.0 | 8942.63 |
Get the cumulative sum of total revenue which determine the relative contribution of each product to the total revenue generated from all products.
Products that belong to the
A class
are those that accounted for80%
of the revenue generated.Class B
products are those that fall between the top 80% and the next15%
of revenue generated.While products in
class C
are those that fall withing the remaining5%
of revenue generated.
Code
def classify_abc_item(cum_percent):
"""
Assign (ABC) classification to each product based on the their percentage of
revenue contribution.
:param cum_percent: The running percentage of revenue contributed.
:return: A pandas Series with three unique category.
"""
if cum_percent <= 80:
return 'A'
elif cum_percent > 80 and cum_percent <= 95:
return 'B'
else:
return 'C'
= abc_df.assign(
abc_df # running total of revenue.
= lambda d: d["total_revenue"].cumsum(),
revenue_cumsum # the percentage of total revenue.
= lambda d: (d["revenue_cumsum"] / d["total_revenue"].sum()) * 100,
revenue_running_percent # aasign each product a class.
= lambda d: d["revenue_running_percent"].apply(classify_abc_item),
abc_class # simple rank to each product based on the its percentage of revenue generated.
= lambda d: d["revenue_running_percent"].rank().astype(int)
rank
)
abc_df.head()
sku | unique_purchases | total_demand | total_revenue | revenue_cumsum | revenue_running_percent | abc_class | rank | |
---|---|---|---|---|---|---|---|---|
0 | 3YDVF | 1 | 926.0 | 29419.35 | 29419.35 | 1.866325 | A | 1 |
1 | LJ26I | 1 | 632.0 | 13571.45 | 42990.80 | 2.727279 | A | 2 |
2 | IRQZ4 | 1 | 321.0 | 11681.63 | 54672.43 | 3.468347 | A | 3 |
3 | DWJC4 | 1 | 823.0 | 10592.09 | 65264.52 | 4.140295 | A | 4 |
4 | 44RUE | 1 | 218.0 | 8942.63 | 74207.15 | 4.707603 | A | 5 |
Summary
= (
temp
abc_df"abc_class")
.groupby(= ('sku', 'nunique'),
.agg(total_products = ('total_demand', "sum"),
total_demand = ('total_revenue', "sum"))
total_revenue
.reset_index() )
Code
def abc_plot(var: str, y_title: str, title: str):
return (
=temp.assign(prop = lambda d: (d[var] / d[var].sum())*100),
g.ggplot(data=g.aes(x="abc_class", y=var)) +
mapping+
g.geom_col() ="prop", y=0),
g.geom_text(g.aes(label=g.position_dodge(width=.9),
position=-0.18,
nudge_x=30, color="#4D4D4D",
size="bottom",
va="{:.1f}%") +
format_string= lambda l: ["{:,.0f}".format(v) for v in l]) +
g.scale_y_continuous(labels ="Class", y=y_title, title=title) +
g.labs(x
custom_theme() )
Number of Products
="total_products",
abc_plot(var="No. Products",
y_title="Number of Products in Each Class") title
A large number of products, e.i about 51.4% fall within the class C
boundary which have the lowest demand and revenue generated while 21.7% of all products falls within the top 80% generating the highest revenue.
Product Demand
="total_demand",
abc_plot(var="Qty. Demand",
y_title="Total Quantity Demand by (ABC) Class") title
Product classification summary by the quantity show how much of products in class A
where in demand. There are no suprises here given that these products generate more revenue. For class C products, despite comprising of 51.4% of all products, they only have 9.9% of the total order quantity.
Revenue
(=temp, mapping=g.aes(x="abc_class", y="total_revenue")) +
g.ggplot(data+
g.geom_col() = lambda l: ["{:,.0f}".format(v) for v in l]) +
g.scale_y_continuous(labels ="Class", y="Revenue", title="Total Revenue by (ABC) Class") +
g.labs(x
custom_theme() )
A visual representation of the 80/15/5 split
of the total revenue generated and as expected all products generating more demand and revenue are in the class A category. It is important for inventory managers to create an optimize inventory level
for such product to meet demand, they should also prioritize supplier relationship
to ensure adequate access to such products, and lastly there should be a huge focus on quality
as anything less than that can lead to decrease in demand and eventually low revenue.
XYZ inventory classification
The major aspect of ABC analysis is it focus on valuable items based on their demand and revenue with less emphasis on other factors such as demand variability and sessionality. This gap can be bridged using the XYZ inventory classification which categorize products according to their level of demand variability.
There are three categories in XYZ analysis which are:
Class X
: These are items with low demand variability, they are less challenging to manage because of their predictable demand patterns.
Class Y
: Items in this class have a moderate demand variability, but they are not easy to manage unlike items in class X, but they have better predictable patterns than items in class Z.
Class Z
: There are high demand variability in items that falls within this category, and demand for product are less stable and predictable.
(=df_m.groupby("month")["quantity"].sum().reset_index(),
g.ggplot(data=g.aes(x="factor(month)", y="quantity", group=1)) +
mapping+
g.geom_line() = lambda l: ["{:,.0f}".format(v) for v in l]) +
g.scale_y_continuous(labels ="Month", y="Quantity", title="Quantity Demand by Month") +
g.labs(x
custom_theme() )
A rundown of the seasonality in the data indicate that the total customer order by month have huge variablility across the year with its lowest point in August
while its peaked in December
. It is crucial to note that there was a surge in order from March to May followed by an abrupt decline in order from June to August.
(=df_m.groupby("month")["sales_amount"].sum().reset_index(),
g.ggplot(data=g.aes(x="factor(month)", y="sales_amount", group=1)) +
mapping+
g.geom_line() = lambda l: ["{:,.0f}".format(v) for v in l]) +
g.scale_y_continuous(labels ="Month", y="Revenue", title="Total Revenue in Each Month") +
g.labs(x
custom_theme() )
Although there are similar seasonal pattern between the total quantity order and revenue, the upword trend in revenue appear to have began in January all the way through to May, before the expected drop in revenue similar to the quantity order.
To draw out the XYZ class, we must first reshape the data and make each row represent a single SKU and the 12 months spread across different columns with values of the total quantity demand for a particular SKU.
= (
xyz_df
df_m"sku", "month"])["quantity"]
.groupby([sum()
.
.reset_index()="sku", columns="month", values="quantity")
.pivot(index0)
.fillna("M")
.add_prefix(
.reset_index()={"month": "index"})
.rename(index
)
= ""
xyz_df.columns.name
xyz_df.head()
sku | M1 | M2 | M3 | M4 | M5 | M6 | M7 | M8 | M9 | M10 | M11 | M12 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 00GVC | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
1 | 00OK1 | 0.0 | 0.0 | 1.0 | 7.0 | 2.0 | 1.0 | 2.0 | 3.0 | 0.0 | 0.0 | 0.0 | 2.0 |
2 | 0121I | 1.0 | 3.0 | 4.0 | 2.0 | 1.0 | 5.0 | 3.0 | 1.0 | 2.0 | 4.0 | 3.0 | 2.0 |
3 | 01IEO | 3.0 | 3.0 | 5.0 | 8.0 | 6.0 | 3.0 | 3.0 | 3.0 | 0.0 | 3.0 | 2.0 | 4.0 |
4 | 01IQT | 0.0 | 2.0 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 2.0 | 0.0 | 0.0 | 0.0 | 1.0 |
= xyz_df.assign(
xyz_df # calculate the standard deviation of all months total demand.
= lambda d: d[[c for c in xyz_df.columns if "M" in c]].std(axis=1),
std_demand # calculate the total demand across all months.
= lambda d: d[[c for c in xyz_df.columns if "M" in c]].sum(axis=1),
total_demand # calculate the average demand for each SKU.
= lambda d: d["total_demand"] / 12,
avg_demand # coefficient of variation for each SKU
= lambda d: d["std_demand"] / d["avg_demand"]
cov_demand
)
xyz_df.head()
sku | M1 | M2 | M3 | M4 | M5 | M6 | M7 | M8 | M9 | M10 | M11 | M12 | std_demand | total_demand | avg_demand | cov_demand | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 00GVC | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.389249 | 2.0 | 0.166667 | 2.335497 |
1 | 00OK1 | 0.0 | 0.0 | 1.0 | 7.0 | 2.0 | 1.0 | 2.0 | 3.0 | 0.0 | 0.0 | 0.0 | 2.0 | 2.022600 | 18.0 | 1.500000 | 1.348400 |
2 | 0121I | 1.0 | 3.0 | 4.0 | 2.0 | 1.0 | 5.0 | 3.0 | 1.0 | 2.0 | 4.0 | 3.0 | 2.0 | 1.311372 | 31.0 | 2.583333 | 0.507628 |
3 | 01IEO | 3.0 | 3.0 | 5.0 | 8.0 | 6.0 | 3.0 | 3.0 | 3.0 | 0.0 | 3.0 | 2.0 | 4.0 | 2.020726 | 43.0 | 3.583333 | 0.563924 |
4 | 01IQT | 0.0 | 2.0 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | 2.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.792961 | 7.0 | 0.583333 | 1.359363 |
Coefficient of variation
: or CV is a statistical measure that express the variability of a set of data as a percentage of its mean.
Formula: CV = (standard deviation / mean)
To measure each SKU variability, its CV will be used to determine the level of variation. So values are closer to zero, it indicate a small amount of variability, while value further from zero indicate a high amount of variability.
("cov_demand"]
xyz_df["min", "mean", "max"])
.agg([
.reset_index()={"index": "Stats", "cov_demand": "Coefficient of Variation"})
.rename(columns )
Stats | Coefficient of Variation | |
---|---|---|
0 | min | 0.095766 |
1 | mean | 1.748931 |
2 | max | 3.464102 |
The average CV of 1.75 indicate that the level of variability in demand is high since it falls within the CV range of 1-2, also the Maximum CV of 3.46 indicate that there are lot of products with high demand variability.
(=xyz_df, mapping=g.aes(x="cov_demand")) +
g.ggplot(data="#FFF", bins = 20) +
g.geom_histogram(color"Coefficient of Variation") +
g.ggtitle(
custom_theme() )
XYZ classes
We will use the 0.5 / 1 / >1 CV to group all product into XYZ classes. products with a CV less than 0.5 indicate low demand variability and better forecating, this category of products will be assgined the X class while product with CV that falls within 0.5 to 1 indicate a moderate demand variability and will make up the Y class. Products with CV greater than 1 are much harder to predict and are subject to high fluctuations throughout the year and they will be categorised as Z class.
Code
def classify_xyz_item(cov):
"""
Assign (XYZ) categories to each SKU based on their coefficient of
variation (CV) in order quantity.
:param cov: Coefficient of variation in order quantity for SKU
:return: A pandas Series with three unique category.
"""
if cov <= 0.5:
return 'X'
elif cov > 0.5 and cov <= 1.0:
return 'Y'
else:
return 'Z'
= xyz_df.assign(xyz_class = xyz_df["cov_demand"].apply(classify_xyz_item))
xyz_df
("xyz_class"]
xyz_df[
.value_counts()
.reset_index()={"index": "Class", "xyz_class": "count"})
.rename(columns )
Class | count | |
---|---|---|
0 | Z | 3429 |
1 | Y | 1281 |
2 | X | 531 |
Based on the number of product in each class, there are more products in the Z class than other classes, this is expected given the CV distribution earlier.
(
xyz_df"xyz_class")
.groupby(= ("sku", "nunique"),
.agg(total_product = ("total_demand", "sum"),
total_demand = ("std_demand", "mean"),
std_of_demand = ("avg_demand", "mean"),
average_demand = ("cov_demand", "mean"))
average_cov
.reset_index() )
xyz_class | total_product | total_demand | std_of_demand | average_demand | average_cov | |
---|---|---|---|---|---|---|
0 | X | 531 | 99436.733 | 4.981240 | 15.605263 | 0.369697 |
1 | Y | 1281 | 65623.300 | 3.037538 | 4.269015 | 0.736462 |
2 | Z | 3429 | 30348.396 | 1.210885 | 0.737542 | 2.340750 |
= [c for c in xyz_df.columns if "M" in c]
month_cols
= (
temp
xyz_df"xyz_class")[month_cols]
.groupby(sum()
.="xyz_class")
.unstack(level
.reset_index()={0: "total_demand", "": "month"})
.rename(columns=lambda d: d["month"].astype("category").cat.reorder_categories(month_cols, ordered=True))
.assign(month
)
temp.head()
month | xyz_class | total_demand | |
---|---|---|---|
0 | M1 | X | 7955.50 |
1 | M1 | Y | 4737.00 |
2 | M1 | Z | 2181.87 |
3 | M2 | X | 7851.49 |
4 | M2 | Y | 4794.34 |
(=temp, mapping=g.aes(x="month", y="total_demand", group=1)) +
g.ggplot(data+
g.geom_line() = lambda l: ["{:,.0f}".format(v) for v in l]) +
g.scale_y_continuous(labels="xyz_class", nrow=3, scales="free_y") +
g.facet_wrap(facets=None, y="Demand", title="(XYZ) Class by Total Quantity Order in each Month") +
g.labs(x=(10, 6))
custom_theme(figure_size )
Examining each category represented on the graph, class X have the least amount of flutuations compared to class Y and class Z, class Z represent a clear case of high volatility on quantity demand across all months.
ABC-XYZ inventory analysis
Combining both ABC and XYZ classes provides inventory managers with a clear insight on the most important items in the inventory, based on the amount of revenue they contribute and also their demand patterns. This information can help them determine the appropriate stock levels which reduces the risk of either stockouts or overstocking.
Merge ABC and XYZ class tables
= abc_df[["sku", "total_revenue", "abc_class"]].merge(
abc_xyz "sku", "std_demand", "total_demand", "avg_demand", "cov_demand", "xyz_class"]],
xyz_df[[="sku",
on="left"
how
)
abc_xyz.head()
sku | total_revenue | abc_class | std_demand | total_demand | avg_demand | cov_demand | xyz_class | |
---|---|---|---|---|---|---|---|---|
0 | 3YDVF | 29419.35 | A | 7.529739 | 926.0 | 77.166667 | 0.097578 | X |
1 | LJ26I | 13571.45 | A | 13.773052 | 632.0 | 52.666667 | 0.261514 | X |
2 | IRQZ4 | 11681.63 | A | 10.703653 | 321.0 | 26.750000 | 0.400137 | X |
3 | DWJC4 | 10592.09 | A | 8.273268 | 823.0 | 68.583333 | 0.120631 | X |
4 | 44RUE | 8942.63 | A | 15.602059 | 218.0 | 18.166667 | 0.858829 | Y |
Combining ABC & XYZ classes
= abc_xyz.assign(abc_xyz_class = abc_xyz["abc_class"].astype(str) + abc_xyz["xyz_class"].astype(str))
abc_xyz
abc_xyz.head()
sku | total_revenue | abc_class | std_demand | total_demand | avg_demand | cov_demand | xyz_class | abc_xyz_class | |
---|---|---|---|---|---|---|---|---|---|
0 | 3YDVF | 29419.35 | A | 7.529739 | 926.0 | 77.166667 | 0.097578 | X | AX |
1 | LJ26I | 13571.45 | A | 13.773052 | 632.0 | 52.666667 | 0.261514 | X | AX |
2 | IRQZ4 | 11681.63 | A | 10.703653 | 321.0 | 26.750000 | 0.400137 | X | AX |
3 | DWJC4 | 10592.09 | A | 8.273268 | 823.0 | 68.583333 | 0.120631 | X | AX |
4 | 44RUE | 8942.63 | A | 15.602059 | 218.0 | 18.166667 | 0.858829 | Y | AY |
This combination create 9 unique classes which are AX, AY, AZ, BX, BY, BZ, CX, CY, and CZ and each SKU falls under one of them. Each combined class can be summarised as follow:
AX
: This category consist of high revenue generating items with a stable demand which makes them easy to forecast.
AY
: Also a high revenue generating items but with less stable demand, as demand varies over time.
AZ
: They are tough to forecaset because of their irregular demand nature, but they are also a high revenue generating item.
BX
: They generate moderate revenue and have a stable demand across all month.
BY
: Items in this category are less stable based on demand but they generate a moderate amount of revenue.
BZ
: Revenue generated from items in this category are moderate but forecasting demand for them can be very challenging.
CX
: Items in this category are easy to forecast but they generate a very low amount of revenue.
CY
: This category includes items with less stable demand and also low revenue.
CZ
: The items here are difficult to forecast because demand may be periodic and they also generate a very small amount of revenue.
= (
temp
abc_xyz"abc_xyz_class")
.groupby(=('sku', 'nunique'),
.agg(total_sku= ('total_demand', "sum"),
total_demand = ('avg_demand', 'mean'),
avg_demand = ('total_revenue', "sum"))
total_revenue
.reset_index()
)
temp
abc_xyz_class | total_sku | total_demand | avg_demand | total_revenue | |
---|---|---|---|---|---|
0 | AX | 412 | 86198.533 | 17.434978 | 622917.97 |
1 | AY | 519 | 38869.630 | 6.241110 | 512348.97 |
2 | AZ | 204 | 5086.510 | 2.077823 | 125490.43 |
3 | BX | 104 | 12588.200 | 10.086699 | 21662.63 |
4 | BY | 575 | 22001.670 | 3.188648 | 103916.87 |
5 | BZ | 731 | 11243.900 | 1.281794 | 111158.97 |
6 | CX | 15 | 650.000 | 3.611111 | 896.88 |
7 | CY | 187 | 4752.000 | 2.117647 | 9454.98 |
8 | CZ | 2494 | 14017.986 | 0.468390 | 68477.72 |
Code
def abc_xyz_plot(var: str, y_title: str, title: str):
return (
=temp, mapping=g.aes(x="abc_xyz_class", y=var)) +
g.ggplot(data+
g.geom_col() ="Class", y=y_title, title=title) +
g.labs(x=lambda l: ["{:,.0f}".format(v) for v in l]) +
g.scale_y_continuous(labels
custom_theme() )
="total_sku",
abc_xyz_plot(var="No. Products",
y_title="Number of Products by (ABC-XYZ) Class") title
47.6% of all products fall under the CZ class
, with this insight inventory managers need to develope a responsive or adaptive supply chain for these products that can instantly adjust to changes in demand, where inventory is oredered and received only when it is needed, which reduces carrying cost.
="total_demand",
abc_xyz_plot(var="Demand",
y_title="Total Quantity Demand by (ABC-XYZ) Class") title
For quantity demand, class AX holds the majority with 44% of all demand
. Since only 7.86% of all product falls under the AX class, inventory managers can bring in more products from other class by reviewing price strategy, imporving marketing and promotion and also consider bundling or cross-selling with other high performing products such as products in class AX and AY.
="total_revenue",
abc_xyz_plot(var="Revenue",
y_title="Total Revenue by (ABC-XYZ) Class") title
Class AX and AY both contains majority of products that generate the highest revenue, a combined 72% of revenue. Inventory managers should consider conducting market research on products generating less revenue to identify trends and customer preferences. This can help them identify new opputunities and adjust product features and marketing accordingly.
In summary ABC-XYZ analysis provides insight that inventory managers can utilize to optimize inventory business operations, by indentifying the most critical items and assigning appproprate inventory control policies, businesses can better manage their stock and ultimately improve customer satisfaction. Additionally, its can help inventory managers to identify opportunities for process improvement, enhance decision-making, and allocate resources effectively.