A little demo to calculate projected inventories & coverages, using the R package planr.

planr is available on :

About planr package:

  • Tools for Supply Chain Management, Demand and Supply Planning

  • Perform flexible and quick calculations for Demand and Supply Planning, such as projected inventories and coverages, as well as replenishment plan.

  • For any time bucket, daily, weekly or monthly, and any granularity level, product or group of products

Upload Libraries

Part 1 : ETL Raw Data

1.1) Get Demand

# Demand
df1 <- read_csv("Demand_Forecasts_DB.csv")
## Rows: 50 Columns: 53
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr  (1): DFU
## dbl (52): 7/3/2022, 7/10/2022, 7/17/2022, 7/24/2022, 7/31/2022, 8/7/2022, 8/...
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# Melt
df1 <- df1 %>% gather(key = "Period", value = "Demand", 2:length(df1))

# Format Date
df1$Period <- as.Date(as.character(df1$Period), format = '%m/%d/%Y')

# remove rows w/ NA in Period
df1 <- df1 %>% drop_na(Period)

# Get Results
Demand_DB <- df1

glimpse(Demand_DB)
## Rows: 2,600
## Columns: 3
## $ DFU    <chr> "Item 000001", "Item 000002", "Item 000003", "Item 000004", "It…
## $ Period <date> 2022-07-03, 2022-07-03, 2022-07-03, 2022-07-03, 2022-07-03, 20…
## $ Demand <dbl> 364, 1419, 265, 1296, 265, 1141, 126, 6859, 66, 380, 7, 61, 128…

1.2) Get Opening Inventories

# get raw data
df1 <- read_csv("Opening_Inventories_DB.csv")
## Rows: 50 Columns: 53
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr  (1): DFU
## dbl (52): 7/3/2022, 7/10/2022, 7/17/2022, 7/24/2022, 7/31/2022, 8/7/2022, 8/...
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# Melt
df1 <- df1 %>% gather(key = "Period", value = "Opening", 2:length(df1))

# Format Date
df1$Period<-as.Date(as.character(df1$Period), format = '%m/%d/%Y')

# get results
Opening_Inventories_DB <- df1 

glimpse(Opening_Inventories_DB)
## Rows: 2,600
## Columns: 3
## $ DFU     <chr> "Item 000001", "Item 000002", "Item 000003", "Item 000004", "I…
## $ Period  <date> 2022-07-03, 2022-07-03, 2022-07-03, 2022-07-03, 2022-07-03, 2…
## $ Opening <dbl> 6570, 5509, 2494, 7172, 1464, 9954, 2092, 17772, 1222, 3411, 2…

1.3) Get Supply Plan

# get raw data
df1 <- read_csv("Supply_Plan_DB.csv")
## Rows: 50 Columns: 53
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr  (1): DFU
## dbl (52): 7/3/2022, 7/10/2022, 7/17/2022, 7/24/2022, 7/31/2022, 8/7/2022, 8/...
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# Melt
df1 <- df1 %>% gather(key = "Period", value = "Supply", 2:length(df1))

# Format Date
df1$Period<-as.Date(as.character(df1$Period), format = '%m/%d/%Y')

# get results
Supply_DB <- df1

glimpse(Supply_DB)
## Rows: 2,600
## Columns: 3
## $ DFU    <chr> "Item 000001", "Item 000002", "Item 000003", "Item 000004", "It…
## $ Period <date> 2022-07-03, 2022-07-03, 2022-07-03, 2022-07-03, 2022-07-03, 20…
## $ Supply <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1585, …

1.4) Combine

# merge
df1 <- left_join(Demand_DB, Opening_Inventories_DB)
df1 <- left_join(df1, Supply_DB)

# Get Results
Blueprint_DB <- df1

head(Blueprint_DB)
## # A tibble: 6 × 5
##   DFU         Period     Demand Opening Supply
##   <chr>       <date>      <dbl>   <dbl>  <dbl>
## 1 Item 000001 2022-07-03    364    6570      0
## 2 Item 000002 2022-07-03   1419    5509      0
## 3 Item 000003 2022-07-03    265    2494      0
## 4 Item 000004 2022-07-03   1296    7172      0
## 5 Item 000005 2022-07-03    265    1464      0
## 6 Item 000006 2022-07-03   1141    9954      0

Part 2 : Calculate Projected Inventories & Coverages

Apply function light_proj_inv

start_time <- Sys.time()

Calculated_PI_DB <- light_proj_inv(data = Blueprint_DB, 
                DFU = DFU, 
                Period = Period, 
                Demand =  Demand, 
                Opening = Opening, 
                Supply = Supply)


end_time <- Sys.time()
calculation.time <- end_time - start_time
calculation.time
## Time difference of 0.645745 secs
head(Calculated_PI_DB)
## # A tibble: 6 × 7
## # Groups:   DFU [1]
##   DFU    Period     Demand Opening Calculated.Coverage.…¹ Projected.Inventorie…²
##   <chr>  <date>      <dbl>   <dbl>                  <dbl>                  <dbl>
## 1 Item … 2022-07-03    364    6570                     17                   6206
## 2 Item … 2022-07-10    364       0                     16                   5842
## 3 Item … 2022-07-17    364       0                     15                   5478
## 4 Item … 2022-07-24    260       0                     14                   5218
## 5 Item … 2022-07-31    736       0                     13                   4482
## 6 Item … 2022-08-07    859       0                     12                   3623
## # ℹ abbreviated names: ¹​Calculated.Coverage.in.Periods,
## #   ²​Projected.Inventories.Qty
## # ℹ 1 more variable: Supply <dbl>

Part 3 : Analyze results

for a single Item

3.1) Table

#--------------
# Select Item
#--------------

# set a working df
df1 <- Calculated_PI_DB

df1 <- as.data.frame(df1)

# filter
df1 <- filter(df1, df1$DFU %in% c("Item 000008"))




#--------------
# Create Table
#--------------

# keep only the needed columns
df1 <- df1 %>% select(Period,
                      Demand,
                      Calculated.Coverage.in.Periods,
                      Projected.Inventories.Qty,
                      Supply)


# create a f_colorpal field
df1 <- df1 %>% mutate(f_colorpal = case_when( Calculated.Coverage.in.Periods > 8 ~ "#FFA500",
                                              Calculated.Coverage.in.Periods > 2 ~ "#32CD32",
                                              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(

                Demand = colDef(
                  name = "Demand (units)",

                  cell = data_bars(df1,
                                   fill_color = "#3fc1c9",
                                   text_position = "outside-end"
                  )

                ),

              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

                `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"
                    )
                  }
                ),

              Supply = colDef(
                name = "Supply (units)",
                cell = data_bars(df1,
                                 fill_color = "#3CB371",
                                 text_position = "outside-end"
                                 )
                )

              ), # close columns lits

              columnGroups = list(
                colGroup(name = "Projected Inventories", columns = c("Calculated.Coverage.in.Periods",
                                                                     "Projected.Inventories.Qty"))

              )

    ) # close reactable

3.2) A little chart

#--------------
# Select Item
#--------------

# set a working df
df1 <- Calculated_PI_DB

df1 <- as.data.frame(df1)

# filter
df1 <- filter(df1, df1$DFU %in% c("Item 000008"))

# subset Period based on those Starting and Ending Periods
df1 <- subset(df1,df1$Period >= "2022-07-03" & df1$Period <= "2022-11-30")



#--------------
# Prepare data
#--------------

# keep only the needed columns
df1 <- df1 %>% select(Period,
                      Projected.Inventories.Qty)

# create a value.index
df1$Value.Index <- if_else(df1$Projected.Inventories.Qty < 0, "Shortage", "Stock")
    
    
# spread
df1 <- df1 %>% spread(Value.Index, 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  

Part 4 : Cockpit

4.1) Create Dataframe

#------------------------------
# Get data
df1 <- Calculated_PI_DB


#------------------------------
# Filter

# subset Period based on those Starting and Ending Periods
df1 <- subset(df1,df1$Period >= "2022-07-03" & df1$Period <= "2022-09-25")


# keep this initial dataset
Initial_DB <- df1





#-----------------
# Get Summary of variables
#-----------------

# set a working df
df1 <- Initial_DB

# aggregate
df1 <- df1 %>% select(DFU, 
                         Demand,
                         Opening,
                         Supply) %>%
      group_by(DFU) %>%
      summarise(Demand = sum(Demand),
                Opening = sum(Opening),
                Supply = sum(Supply)
      )
    
# let's calculate the share of Demand
df1$Demand.pc <- df1$Demand / sum(df1$Demand)
    
    
# keep Results
Value_DB <- df1
    

 
    
#-----------------
# Get Sparklines Demand
#-----------------
    
# set a working df
df1 <- Initial_DB
    
# replace missing values by zero
df1$Demand[is.na(df1$Demand)] <- 0
    
# aggregate
df1 <- df1 %>%
      group_by(
        DFU,
        Period
      ) %>%
      summarise(
        Quantity = sum(Demand)
      )
    
    # generate Sparkline
    df1 <- df1 %>%
      group_by(DFU) %>%
      summarise(Demand.Quantity = list(Quantity))
    
# keep Results
Demand_Sparklines_DB <- df1

    
#-----------------
# Get Sparklines Supply
#-----------------
    
# set a working df
df1 <- Initial_DB
    
# replace missing values by zero
df1$Supply[is.na(df1$Supply)] <- 0
    
# aggregate
df1 <- df1 %>%
      group_by(
        DFU,
        Period
      ) %>%
      summarise(
        Quantity = sum(Supply)
      )
    
    # generate Sparkline
    df1 <- df1 %>%
      group_by(DFU) %>%
      summarise(Supply.Quantity = list(Quantity))
    
# keep Results
Supply_Sparklines_DB <- df1






#-----------------
# Get Sparklines Projected Inventories
#-----------------
    
# set a working df
df1 <- Initial_DB
    
# replace missing values by zero
df1$Projected.Inventories.Qty[is.na(df1$Projected.Inventories.Qty)] <- 0
    
# aggregate
df1 <- df1 %>%
      group_by(
        DFU,
        Period
      ) %>%
      summarise(
        Quantity = sum(Projected.Inventories.Qty)
      )
    
    # generate Sparkline
    df1 <- df1 %>%
      group_by(DFU) %>%
      summarise(PI.Quantity = list(Quantity))
    
# keep Results
PI_Sparklines_DB <- df1





#--------
# Create a Delay.Analysis check
#--------

# set a working df
df1 <- Initial_DB

# aggregate
df1 <- df1 %>% select(DFU, Period,Projected.Inventories.Qty) %>%
      group_by(DFU) %>%
      summarise(min.Projected.Inventories.Qty = min(Projected.Inventories.Qty),
                max.Projected.Inventories.Qty = max(Projected.Inventories.Qty)
      )



#-----------------
# Identify where we are late to supply
#-----------------

# Add a character info to analyze whether there is an identified delay or not
df1$Delay.Analysis <- if_else(df1$min.Projected.Inventories.Qty <= 0, "Delay", "OK")

# Get Results
Check_DB <- df1








#-----------------
# Merge dataframes
#-----------------

# merge
df1 <- left_join(Value_DB, Demand_Sparklines_DB)
df1 <- left_join(df1, Supply_Sparklines_DB)
df1 <- left_join(df1, PI_Sparklines_DB)
df1 <- left_join(df1, Check_DB)


# reorder columns
df1 <- df1 %>% select(DFU, Demand, Demand.pc, Demand.Quantity,
                      Supply, Supply.Quantity,
                      Opening,
                      PI.Quantity,
                      Delay.Analysis)


# get results
Summary_DB <- df1

glimpse(Summary_DB)
## Rows: 50
## Columns: 9
## $ DFU             <chr> "Item 000001", "Item 000002", "Item 000003", "Item 000…
## $ Demand          <dbl> 6185, 18458, 1314, 12336, 1546, 17846, 3870, 49416, 90…
## $ Demand.pc       <dbl> 0.012262677, 0.036595714, 0.002605199, 0.024457944, 0.…
## $ Demand.Quantity <list> <364, 364, 364, 260, 736, 859, 859, 859, 273, 349, 34…
## $ Supply          <dbl> 0, 15120, 6733, 11713, 1187, 17556, 2593, 25709, 0, 25…
## $ Supply.Quantity <list> <0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>, <0, 0, 0, 10…
## $ Opening         <dbl> 6570, 5509, 2494, 7172, 1464, 9954, 2092, 17772, 1222,…
## $ PI.Quantity     <list> <6206, 5842, 5478, 5218, 4482, 3623, 2764, 1905, 1632…
## $ Delay.Analysis  <chr> "OK", "OK", "OK", "Delay", "OK", "OK", "OK", "Delay", …

4.2) Display Table

Let’s create a function to display a badge :

#----------------------------------------------------------------------
#    A Function to define a Badge Status in the reactable
#----------------------------------------------------------------------

status_badge <- function(color = "#aaa", width = "9px", height = width) {
  span(style = list(
    display = "inline-block",
    marginRight = "8px",
    width = width,
    height = height,
    backgroundColor = color,
    borderRadius = "50%"
  ))
}

Now let’s create a reactable :

reactable(df1,compact = TRUE,
              
              defaultSortOrder = "desc",
              defaultSorted = c("Demand"),
              defaultPageSize = 20,
              
              columns = list(
                
                `DFU` = colDef(name = "DFU"),

                
                `Demand`= colDef(
                  name = "Total Demand (units)",
                  aggregate = "sum", footer = function(values) formatC(sum(values),format="f", big.mark=",", digits=0),
                  format = colFormat(separators = TRUE, digits=0),
                  style = list(background = "yellow",fontWeight = "bold")
                ),
                
                
                `Demand.pc`= colDef(
                  name = "Share of Demand (%)",
                  format = colFormat(percent = TRUE, digits = 1)
                ), # close %
                
                
                `Supply`= colDef(
                  name = "Total Supply (units)",
                  aggregate = "sum", footer = function(values) formatC(sum(values),format="f", big.mark=",", digits=0),
                  format = colFormat(separators = TRUE, digits=0)
                ),
                
                
                
                `Opening`= colDef(
                  name = "Opening Inventories (units)",
                  aggregate = "sum", footer = function(values) formatC(sum(values),format="f", big.mark=",", digits=0),
                  format = colFormat(separators = TRUE, digits=0)
                ),
                
                
                Demand.Quantity = colDef(
                  name = "Projected Demand",
                  cell = function(value, index) {
                    sparkline(df1$Demand.Quantity[[index]])
                  }),
                

                
                
                Supply.Quantity = colDef(
                  name = "Projected Supply",
                  cell = function(values) {
                    sparkline(values, type = "bar"
                              #chartRangeMin = 0, chartRangeMax = max(chickwts$weight)
                    )
                  }),
                
                
                PI.Quantity = colDef(
                  name = "Projected Inventories",
                  cell = function(values) {
                    sparkline(values, type = "bar"
                              #chartRangeMin = 0, chartRangeMax = max(chickwts$weight)
                    )
                  }),
                
                
                
                Delay.Analysis = colDef(
                  name = "Delay Analysis",
                  
                  cell = function(value) {
                    color <- switch(
                      value,
                      OK = "hsl(120,61%,50%)",
                      Delay = "hsl(39,100%,50%)"
                    )
                    badge <- status_badge(color = color)
                    tagList(badge, value)
                  })
                


 
                
                
              ), # close columns list
              
              defaultColDef = colDef(footerStyle = list(fontWeight = "bold")),
              
              
              columnGroups = list(
                
                colGroup(name = "Demand",
                         columns = c("Demand",
                                     "Demand.pc",
                                     "Demand.Quantity")),
                
                colGroup(name = "Supply",
                         columns = c("Supply", "Supply.Quantity")),
                
                
                colGroup(name = "Inventories",
                         columns = c("Opening", "PI.Quantity", "Delay.Analysis"))
                
                
              )
          
) # close reactable

This cockpit gives us a quick overview about the risks of delays (negative projected inventories). However, we don’t know :

  • about the possible overstocks

  • whether those delays, or overstocks, are significant versus some targets

We can then introduce 2 new parameters :

  • Min.Cov : Minimum Coverage target, expressed in Period

  • Max.Cov : Maximum Coverage target, expressed in Periods

And calculate the projected inventories and coverages using the proj_inv() function.

Then, we’ll be able to compare the projected coverages versus those 2 target levels.