# ETL
library(tidyverse)
# for the tables
library(reactable)
library(reactablefmtr)
# for the charts
library(highcharter)
# for the Demand & Supply Planning calculations : the library planr
library(planr)
# Others
library(htmltools)
one item
First, let’s load a few libraries :
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.
1) Create Data Template
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.
<- c(
Period "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")
<- 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)
Demand
<- 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)
Opening
<- 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)
Supply
# assemble
<- data.frame(Period,
my_demand_and_suppply
Demand,
Opening,
Supply)
# let's add a Product
$DFU <- "Product A"
my_demand_and_suppply
# format the Period as a date
$Period <- as.Date(as.character(my_demand_and_suppply$Period), format = '%m/%d/%Y')
my_demand_and_suppply
# 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
<- 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")
df1
# get Results
<- df1
my_drp_template
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
2) DRP Calculation
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.
- Maximum Stock = Safety Stocks (SSCov) + Frequency of Supply (DRPCovDur)
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
- in the Frozen Horizon : the existing plan, which is “frozen”
- in the Free Horizon : the calculated plan, based on the parameters (SSCov, DRPCovDur, MOQ)
- in the Frozen Horizon : the existing plan, which is “frozen”
# set a working df
<- my_drp_template
df1
# calculate drp
<- planr::drp(data = df1,
demo_drp DFU = DFU,
Period = Period,
Demand = Demand,
Opening = Opening,
Supply = Supply,
SSCov = SSCov,
DRPCovDur = DRPCovDur,
MOQ = MOQ,
FH = FH)
# formatting
<- as.data.frame(demo_drp)
demo_drp
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,…
3) Table
First, let’s create a function status_PI.Index()
# create a function status.PI.Index
<- function(color = "#aaa", width = "0.55rem", height = width) {
status_PI.Index 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
<- demo_drp
df1
# keep only the needed columns
<- df1 |> select(Period, FH, Demand, DRP.Calculated.Coverage.in.Periods, DRP.Projected.Inventories.Qty, DRP.plan)
df1
# replace missing values by zero
$DRP.plan <- df1$DRP.plan |> replace_na(0)
df1$DRP.Projected.Inventories.Qty <- df1$DRP.Projected.Inventories.Qty |> replace_na(0)
df1
# create a f_colorpal field
<- df1 |> mutate(f_colorpal = case_when( DRP.Calculated.Coverage.in.Periods > 8 ~ "#FFA500",
df1 > 2 ~ "#32CD32",
DRP.Calculated.Coverage.in.Periods > 0 ~ "#FFFF99",
DRP.Calculated.Coverage.in.Periods 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) {
<- switch(
color
value,Free = "hsl(154, 64%, 50%)",
Frozen = "hsl(3, 69%, 50%)"
)<- status_PI.Index(color = color)
PI.Index 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) {
<- "#008000"
color else if (value < 0) {
} <- "#e00000"
color else {
} <- "#777"
color
}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 )
4) A little chart
We can also visualize the result through a chart, using the library highcharter.
a) Line Chart
# set a working df
<- demo_drp
df1
# Chart
<- highchart() |>
p
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
b) Bar Chart
With Positive & Negative Projected Inventories.
# set a working df
<- demo_drp
df1
# keep only the needed columns
<- df1 |> select(Period, DRP.Projected.Inventories.Qty)
df1
# create a value.index
$Value.Index <- if_else(df1$DRP.Projected.Inventories.Qty < 0, "Shortage", "Stock")
df1
# spread
<- df1 |> spread(Value.Index, DRP.Projected.Inventories.Qty)
df1
#----------------------------------------------------
# Chart
<- highchart() |>
u 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