First, let’s load a few libraries :
# ETL
library(tidyverse)
library(scales)
# for the tables
library(reactable)
library(reactablefmtr)
# for the charts
library(highcharter)
# for the Demand & Supply Planning calculations : the library planr
library(planr)
library(htmltools)
We’re going to present here how to use the drp() function from the R package planr.
More info on : https://github.com/nguyennico/planr
This function allows to calculate a Replenishment Plan, also called DRP (Distribution Requirement Planning).
We need a dataset with the 5 classic variables to use this function :
a Product: it’s an item, a SKU (Storage Keeping Unit), or a SKU at a location, also called a DFU (Demand Forecast Unit)
a Period of time : for example monthly or weekly buckets
a Demand : could be some sales forecasts, expressed in units
an Opening Inventory : what we hold as available inventories at the beginning of the horizon, expressed in units
a Supply Plan : the supplies that we plan to receive, expressed in units
And a few new parameters added to this initial dataset :
SSCov : the Safety Stock Coverage, expressed in number of periods
DRPCovDur : the Frequency of Supply, expressed in number of periods
MOQ : the Multiple Order Quantity, expressed in units, 1 by default or a multiple of a Minimum Order Quantity
FH : defines the Frozen and Free Horizon. It has 2 values: Frozen or Free. If Frozen : no calculation of Replenishment Plan yet, the calculation starts when the period is defined as Free. We can use this parameter to consider some defined productions plans or supplies (allocations, workorders,…) in the short-term for example.
We are going to :
apply the drp() on this simple template
create 2 nice visuals of tables and charts, using the R packages reactable, reactablefmtr and highcharter.
First, let’s create a dataset which contains the basic features, i.e. the 5 variables :
Period
Demand
Opening
Suply
DFU
We’ll call it my_demand_and_suppply.
Period <- c(
"1/1/2020", "2/1/2020", "3/1/2020", "4/1/2020", "5/1/2020", "6/1/2020", "7/1/2020", "8/1/2020", "9/1/2020", "10/1/2020", "11/1/2020", "12/1/2020","1/1/2021", "2/1/2021", "3/1/2021", "4/1/2021", "5/1/2021", "6/1/2021", "7/1/2021", "8/1/2021", "9/1/2021", "10/1/2021", "11/1/2021", "12/1/2021")
Demand <- c(360, 458,300,264,140,233,229,208,260,336,295,226,336,434,276,240,116,209,205,183,235,312,270,201)
Opening <- c(1310,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
Supply <- c(0,0,0,0,0,2500,0,0,0,0,0,0,0,0,0,2000,0,0,0,0,0,0,0,0)
# assemble
my_demand_and_suppply <- data.frame(Period,
Demand,
Opening,
Supply)
# let's add a Product
my_demand_and_suppply$DFU <- "Product A"
# format the Period as a date
my_demand_and_suppply$Period <- as.Date(as.character(my_demand_and_suppply$Period), format = '%m/%d/%Y')
# let's have a look at it
head(my_demand_and_suppply)
## Period Demand Opening Supply DFU
## 1 2020-01-01 360 1310 0 Product A
## 2 2020-02-01 458 0 0 Product A
## 3 2020-03-01 300 0 0 Product A
## 4 2020-04-01 264 0 0 Product A
## 5 2020-05-01 140 0 0 Product A
## 6 2020-06-01 233 0 2500 Product A
Now, let’s add the 4 new parameters, that we will use to calculate the DRP :
SSCov : here 2 periods, means that we want to keep always, as a safety stocks, 2 months of coverage
DRPCovDur : here 3 periods, means that we want to replenish every 3 periods (it’s the frequency of replenishment)
MOQ : here 1 unit, by default, so we keep a flexible Minimum Order Quantity
FH : here 6 first periods defined as Frozen, which means that the DRP calculation will start from the Period n°7, mentioned as Free
df1 <- my_demand_and_suppply
df1$SSCov <- 2
df1$DRPCovDur <- 3
df1$MOQ <- 1
df1$FH <- c("Frozen", "Frozen", "Frozen", "Frozen","Frozen","Frozen","Free","Free","Free","Free","Free","Free","Free","Free","Free","Free","Free","Free","Free","Free","Free","Free","Free","Free")
# get Results
my_drp_template <- df1
head(my_drp_template)
## Period Demand Opening Supply DFU SSCov DRPCovDur MOQ FH
## 1 2020-01-01 360 1310 0 Product A 2 3 1 Frozen
## 2 2020-02-01 458 0 0 Product A 2 3 1 Frozen
## 3 2020-03-01 300 0 0 Product A 2 3 1 Frozen
## 4 2020-04-01 264 0 0 Product A 2 3 1 Frozen
## 5 2020-05-01 140 0 0 Product A 2 3 1 Frozen
## 6 2020-06-01 233 0 2500 Product A 2 3 1 Frozen
Now let’s apply the drp() function to this dataset :
the initial dataset, my_drp_template, has 9 variables
the new one, called below demo_drp, will have 15 variables
The 5 new variables are :
Safety.Stocks : the projected safety stocks, in units
Maximum.Stocks : the projected maximum stocks, in units.
DRP.Calculated.Coverage.in.Periods : the calculated projected inventories, expressed in periods of coverage
DRP.Projected.Inventories.Qty : the calculated projected inventories, based on the [DRP.plan]
DRP.plan : the calculated Replenishment Plan
# calculate
demo_drp <- planr::drp(data = my_drp_template,
DFU = DFU,
Period = Period,
Demand = Demand,
Opening = Opening,
Supply = Supply,
SSCov = SSCov,
DRPCovDur = DRPCovDur,
MOQ = MOQ,
FH = FH)
glimpse(demo_drp)
## Rows: 24
## Columns: 15
## $ DFU <chr> "Product A", "Product A", "Product …
## $ Period <date> 2020-01-01, 2020-02-01, 2020-03-01…
## $ Demand <dbl> 360, 458, 300, 264, 140, 233, 229, …
## $ Opening <dbl> 1310, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ Supply <dbl> 0, 0, 0, 0, 0, 2500, 0, 0, 0, 0, 0,…
## $ SSCov <dbl> 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,…
## $ DRPCovDur <dbl> 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,…
## $ Stock.Max <dbl> 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,…
## $ MOQ <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
## $ FH <chr> "Frozen", "Frozen", "Frozen", "Froz…
## $ Safety.Stocks <dbl> 758, 564, 404, 373, 462, 437, 468, …
## $ Maximum.Stocks <dbl> 1395, 1166, 1074, 1070, 1266, 1328,…
## $ DRP.Calculated.Coverage.in.Periods <dbl> 2.7, 1.7, 0.7, -0.5, -0.9, 7.4, 6.4…
## $ DRP.Projected.Inventories.Qty <dbl> 950, 492, 192, -72, -212, 2055, 182…
## $ DRP.plan <dbl> 0, 0, 0, 0, 0, 2500, 0, 0, 0, 0, 0,…
First, let’s create a function status_PI.Index()
# create a function status.PI.Index
status_PI.Index <- function(color = "#aaa", width = "0.55rem", height = width) {
span(style = list(
display = "inline-block",
marginRight = "0.5rem",
width = width,
height = height,
backgroundColor = color,
borderRadius = "50%"
))
}
Now let’s create a table, using the packages reactable and reactablefmtr :
# set a working df
df1 <- demo_drp
# keep only the needed columns
df1 <- df1 |> select(Period, FH, Demand, DRP.Calculated.Coverage.in.Periods, DRP.Projected.Inventories.Qty, DRP.plan)
# replace missing values by zero
df1$DRP.plan <- df1$DRP.plan |> replace_na(0)
df1$DRP.Projected.Inventories.Qty <- df1$DRP.Projected.Inventories.Qty |> replace_na(0)
# create a f_colorpal field
df1 <- df1 |> mutate(f_colorpal = case_when( DRP.Calculated.Coverage.in.Periods > 8 ~ "#FFA500",
DRP.Calculated.Coverage.in.Periods > 2 ~ "#32CD32",
DRP.Calculated.Coverage.in.Periods > 0 ~ "#FFFF99",
TRUE ~ "#FF0000" ))
# create reactable
reactable(df1, resizable = TRUE, showPageSizeOptions = TRUE,
striped = TRUE, highlight = TRUE, compact = TRUE,
defaultPageSize = 20,
columns = list(
FH = colDef(
name = "Frozen Horizon",
cell = function(value) {
color <- switch(
value,
Free = "hsl(154, 64%, 50%)",
Frozen = "hsl(3, 69%, 50%)"
)
PI.Index <- status_PI.Index(color = color)
tagList(PI.Index, value)
}),
Demand = colDef(
name = "Demand (units)",
cell = data_bars(df1,
fill_color = "#3fc1c9",
text_position = "outside-end"
)
),
DRP.Calculated.Coverage.in.Periods = colDef(
name = "Coverage (Periods)",
maxWidth = 90,
cell= color_tiles(df1, color_ref = "f_colorpal")
),
f_colorpal = colDef(show = FALSE), # hidden, just used for the coverages
`DRP.Projected.Inventories.Qty`= colDef(
name = "Projected Inventories (units)",
format = colFormat(separators = TRUE, digits=0),
style = function(value) {
if (value > 0) {
color <- "#008000"
} else if (value < 0) {
color <- "#e00000"
} else {
color <- "#777"
}
list(color = color
#fontWeight = "bold"
)
}
),
DRP.plan = colDef(
name = "Replenishment (units)",
cell = data_bars(df1,
fill_color = "#3CB371",
text_position = "outside-end"
)
)
), # close columns lits
columnGroups = list(
colGroup(name = "Projected Inventories", columns = c("DRP.Calculated.Coverage.in.Periods",
"DRP.Projected.Inventories.Qty"))
)
) # close reactable
We can also visualize the result through a chart, using the library highcharter.
# set a working df
df1 <- demo_drp
# Chart
p <- highchart() |>
hc_add_series(name = "Max", color = "crimson", data = df1$Maximum.Stocks) |>
hc_add_series(name = "min", color = "lightblue", data = df1$Safety.Stocks) |>
hc_add_series(name = "Projected Inventories", color = "gold", data = df1$DRP.Projected.Inventories.Qty) |>
hc_title(text = "(DRP) Projected Inventories") |>
hc_subtitle(text = "in units") |>
hc_xAxis(categories = df1$Period) |>
#hc_yAxis(title = list(text = "Sales (units)")) %>%
hc_add_theme(hc_theme_google())
p
With Positive & Negative Projected Inventories.
# set a working df
df1 <- demo_drp
# keep only the needed columns
df1 <- df1 |> select(Period, DRP.Projected.Inventories.Qty)
# create a value.index
df1$Value.Index <- if_else(df1$DRP.Projected.Inventories.Qty < 0, "Shortage", "Stock")
# spread
df1 <- df1 |> spread(Value.Index, DRP.Projected.Inventories.Qty)
#----------------------------------------------------
# Chart
u <- highchart() |>
hc_title(text = "Projected Inventories") |>
hc_subtitle(text = "in units") |>
hc_add_theme(hc_theme_google()) |>
hc_xAxis(categories = df1$Period) |>
hc_add_series(name = "Stock",
color = "#32CD32",
#dataLabels = list(align = "center", enabled = TRUE),
data = df1$Stock) |>
hc_add_series(name = "Shortage",
color = "#dc3220",
#dataLabels = list(align = "center", enabled = TRUE),
data = df1$Shortage) |>
hc_chart(type = "column") |>
hc_plotOptions(series = list(stacking = "normal"))
u
Useful links :
github R planr package : nguyennico/planr (github.com)
R cran : CRAN - Package planr (r-project.org)
R planr package website : planr - About (quarto.pub)
The R package planr provides some tools for Supply Chain management, to perform calculations related to Demand & Supply Planning or S&OP (Sales & Operations Planning) process.