HeatWave Analysis: Demand Forecasting & Inventory Control

if (!require("forecast")) install.packages("forecast")
library(forecast)

#1
# demand HeatWave (2019 - 2023)
demanda <- c(
  3610, 3390, 3770, 4370, 5010, 5390, 5670, 5920, 5880, 5500, 4720, 3810,
  3600, 3400, 3760, 4530, 5350, 5950, 6280, 6560, 6530, 6310, 5190, 4380,
  4360, 3860, 4300, 5130, 5860, 6450, 6340, 6990, 7170, 7070, 5690, 4980,
  4630, 4190, 4560, 5400, 6200, 6920, 7230, 7570, 7810, 7790, 6830, 5880,
  5010, 5020, 5480, 6250, 7140, 7970, 7840, 7830, 7440, 6910, 5440, 4930
)

y <- ts(demanda, start = c(2019, 1), frequency = 12)

# models
m_ses   <- ses(y)                                # Simple Exponential Smoothing
m_holt  <- holt(y)                               # Holt's Linear Trend
m_trend <- tslm(y ~ trend)                       # Linear Trend Analysis
m_reg   <- tslm(y ~ trend + season)              # Regresión con Dummies estacionales
m_hw    <- hw(y, seasonal = "multiplicative")    # Holt-Winters Multiplicativo

#
fit_ma <- rep(NA, length(y))
for(i in 13:length(y)) fit_ma[i] <- mean(y[(i-12):(i-1)])
err_ma <- y - fit_ma
mape_ma <- mean(abs(err_ma / y)) * 100
mad_ma  <- mean(abs(err_ma))
msd_ma  <- mean(err_ma^2)

#
acc_ses   <- accuracy(m_ses)
acc_holt  <- accuracy(m_holt)
acc_trend <- accuracy(m_trend)
acc_reg   <- accuracy(m_reg)
acc_hw    <- accuracy(m_hw)

#
tabla_comparativa <- data.frame(
  Method = c(
    "Moving Average (N=12)",
    "Simple Exp. Smoothing (SES)",
    "Holt's Linear Trend",
    "Linear Trend Analysis",
    "Multiple Regression (Trend + Dummies)",
    "Holt-Winters (Multiplicative)"
  ),
  Seasonality = c("No", "No", "No", "No", "Yes", "Yes"),
  MAPE = round(c(mape_ma, acc_ses[,"MAPE"], acc_holt[,"MAPE"], acc_trend[,"MAPE"], acc_reg[,"MAPE"], acc_hw[,"MAPE"]), 2),
  MAD  = round(c(mad_ma, acc_ses[,"MAE"], acc_holt[,"MAE"], acc_trend[,"MAE"], acc_reg[,"MAE"], acc_hw[,"MAE"]), 2),
  MSD  = round(c(msd_ma, acc_ses[,"RMSE"]^2, acc_holt[,"RMSE"]^2, acc_trend[,"RMSE"]^2, acc_reg[,"RMSE"]^2, acc_hw[,"RMSE"]^2), 2)
)

print(tabla_comparativa)
##                                  Method Seasonality  MAPE    MAD        MSD
## 1                 Moving Average (N=12)          No    NA     NA         NA
## 2           Simple Exp. Smoothing (SES)          No  9.33 501.04  367382.08
## 3                   Holt's Linear Trend          No  7.38 383.88  244085.08
## 4                 Linear Trend Analysis          No 17.93 946.59 1113028.08
## 5 Multiple Regression (Trend + Dummies)         Yes  4.01 224.08   96343.54
## 6         Holt-Winters (Multiplicative)         Yes  2.53 141.49   33154.61
checkresiduals(m_hw)

## 
##  Ljung-Box test
## 
## data:  Residuals from Holt-Winters' multiplicative method
## Q* = 5.2192, df = 12, p-value = 0.9503
## 
## Model df: 0.   Total lags used: 12
checkresiduals(m_reg)

## 
##  Breusch-Godfrey test for serial correlation of order up to 16
## 
## data:  Residuals from Linear regression model
## LM test = 44.864, df = 16, p-value = 0.0001456
#2
pronostico_2024 <- forecast(m_hw, h = 12)

tabla_2024 <- data.frame(
  Month = c("Jan 2024", "Feb 2024", "Mar 2024", "Apr 2024", "May 2024", "Jun 2024",
            "Jul 2024", "Aug 2024", "Sep 2024", "Oct 2024", "Nov 2024", "Dec 2024"),
  Forecast = round(as.numeric(pronostico_2024$mean))
)

# Print table
print(tabla_2024)
##       Month Forecast
## 1  Jan 2024     4512
## 2  Feb 2024     4203
## 3  Mar 2024     4598
## 4  Apr 2024     5368
## 5  May 2024     6169
## 6  Jun 2024     6841
## 7  Jul 2024     6967
## 8  Aug 2024     7268
## 9  Sep 2024     7260
## 10 Oct 2024     6998
## 11 Nov 2024     5819
## 12 Dec 2024     5008
plot(pronostico_2024,
     PI = FALSE,
     main = "HeatWave: Demand Forecast 2024 (Holt-Winters Multiplicative)",
     xlab = "Year",
     ylab = "Demand (Units)",
     col = "black",
     fcol = "pink",
     flwd = 2)

lines(
  x = c(time(pronostico_2024$x)[length(pronostico_2024$x)], time(pronostico_2024$mean)[1]),
  y = c(tail(pronostico_2024$x, 1), pronostico_2024$mean[1]),
  col = "black",
  lwd = 2
)

#3
# HEATWAVE APPLIANCES
# FINISHED-GOODS INVENTORY POLICY
# ==========================================================

# ----------------------------------------------------------
# 1. EOQ FUNCTION
# ----------------------------------------------------------

EOQ <- function(D, S, H){
  Q <- sqrt((2 * D * S) / H)
  return(Q)
}

# ----------------------------------------------------------
# 2. 2024 FORECAST
# ----------------------------------------------------------

forecast_2024 <- c(
  4512,  # January
  4203,  # February
  4598,  # March
  5368,  # April
  6169,  # May
  6841,  # June
  6967,  # July
  7268,  # August
  7260,  # September
  6998,  # October
  5819,  # November
  5008   # December
)

# ----------------------------------------------------------
# 3. INVENTORY PARAMETERS
# ----------------------------------------------------------

D <- sum(forecast_2024)

S <- 10000

C <- 5000

monthly_holding_rate <- 0.02

H <- C * monthly_holding_rate * 12

# ----------------------------------------------------------
# 4. DEMAND UNCERTAINTY
# ----------------------------------------------------------

MSD <- 33155

sigma_month <- sqrt(MSD)

# Assumption: one-month finished-goods lead time
L <- 1

sigma_LT <- sigma_month * sqrt(L)

# ----------------------------------------------------------
# 5. SAFETY STOCK
# ----------------------------------------------------------

z95 <- qnorm(0.95)

z99 <- qnorm(0.99)

SS95_exact <- z95 * sigma_LT
SS99_exact <- z99 * sigma_LT

SS95 <- ceiling(SS95_exact)
SS99 <- ceiling(SS99_exact)

additional_SS <- SS99 - SS95

# ----------------------------------------------------------
# 6. ECONOMIC ORDER QUANTITY
# ----------------------------------------------------------

EOQ_exact <- EOQ(D, S, H)

Q <- round(EOQ_exact)

# ----------------------------------------------------------
# 7. OPERATING METRICS
# ----------------------------------------------------------

orders_per_year <- D / Q

order_cycle_days <- 365 / orders_per_year

cycle_inventory <- Q / 2

# ----------------------------------------------------------
# 8. REORDER POINT
# ----------------------------------------------------------

ROP95 <- forecast_2024 + SS95

ROP99 <- forecast_2024 + SS99

ROP_table <- data.frame(
  Month = month.name,
  Forecast = forecast_2024,
  ROP_95 = ROP95,
  ROP_99 = ROP99
)

# ----------------------------------------------------------
# 9. ANNUAL INVENTORY COST
# ----------------------------------------------------------

annual_ordering_cost <- (D / Q) * S

cycle_holding_cost <- (Q / 2) * H

safety_holding_95 <- SS95 * H
safety_holding_99 <- SS99 * H

holding_cost_95 <- cycle_holding_cost + safety_holding_95
holding_cost_99 <- cycle_holding_cost + safety_holding_99

TRC95 <- annual_ordering_cost + holding_cost_95
TRC99 <- annual_ordering_cost + holding_cost_99

additional_annual_cost <- TRC99 - TRC95

additional_inventory_value <- additional_SS * C

# ----------------------------------------------------------
# 10. DISPLAY FINAL RESULTS
# ----------------------------------------------------------

cat("Annual demand:", D, "\n")
## Annual demand: 71011
cat("Annual holding cost/unit:", H, "\n")
## Annual holding cost/unit: 1200
cat("Forecast standard deviation:", sigma_month, "\n\n")
## Forecast standard deviation: 182.0851
cat("Safety stock 95%:", SS95, "\n")
## Safety stock 95%: 300
cat("Safety stock 99%:", SS99, "\n")
## Safety stock 99%: 424
cat("Additional safety stock:", additional_SS, "\n\n")
## Additional safety stock: 124
cat("Exact EOQ:", EOQ_exact, "\n")
## Exact EOQ: 1087.896
cat("Rounded EOQ:", Q, "\n")
## Rounded EOQ: 1088
cat("Orders per year:", orders_per_year, "\n")
## Orders per year: 65.26746
cat("Average order cycle:", order_cycle_days, "days\n")
## Average order cycle: 5.592373 days
cat("Cycle inventory:", cycle_inventory, "\n\n")
## Cycle inventory: 544
cat("TRC 95%:", TRC95, "\n")
## TRC 95%: 1665475
cat("TRC 99%:", TRC99, "\n")
## TRC 99%: 1814275
cat("Additional annual cost:", additional_annual_cost, "\n")
## Additional annual cost: 148800
cat("Additional inventory value:", additional_inventory_value, "\n\n")
## Additional inventory value: 620000
print(ROP_table)
##        Month Forecast ROP_95 ROP_99
## 1    January     4512   4812   4936
## 2   February     4203   4503   4627
## 3      March     4598   4898   5022
## 4      April     5368   5668   5792
## 5        May     6169   6469   6593
## 6       June     6841   7141   7265
## 7       July     6967   7267   7391
## 8     August     7268   7568   7692
## 9  September     7260   7560   7684
## 10   October     6998   7298   7422
## 11  November     5819   6119   6243
## 12  December     5008   5308   5432

Quantity Discount Evaluation

# ==============================================================================
# HEATWAVE: QUANTITY DISCOUNT MODEL EVALUATION
# ==============================================================================

# 1. Base Parameters
D <- 71011

# 2. Magnetron Calculations
eoq_mag <- sqrt((2 * D * 10000) / (1150 * 0.24)) # EOQ Tier 2 = 2268
trc_mag_2268 <- (D * 1150) + (D / 2268) * 10000 + (2268 / 2) * 1150 * 0.24
trc_mag_2500 <- (D * 1130) + (D / 2500) * 10000 + (2500 / 2) * 1130 * 0.24
trc_mag_10k  <- (D * 1110) + (D / 10000) * 10000 + (10000 / 2) * 1110 * 0.24

purch_mag <- c(D * 1150, D * 1130, D * 1110)
order_mag <- c((D / 2268) * 10000, (D / 2500) * 10000, (D / 10000) * 10000)
hold_mag  <- c((2268 / 2) * 1150 * 0.24, (2500 / 2) * 1130 * 0.24, (10000 / 2) * 1110 * 0.24)

# 3. Transformer WCP Calculations
eoq_trans <- sqrt((2 * D * 15000) / (1660 * 0.24)) # EOQ Tier 3 = 2312
trc_trans_2312 <- (D * 1660) + (D / 2312) * 15000 + (2312 / 2) * 1660 * 0.24

purch_trans <- D * 1660
order_trans <- (D / 2312) * 15000
hold_trans  <- (2312 / 2) * 1660 * 0.24

# 4. Cooking Cavity Calculations
trc_cavity_10k <- (D * 1280) + (D / 10000) * 2500 + (10000 / 2) * 1280 * 0.18

purch_cavity <- D * 1280
order_cavity <- (D / 10000) * 2500
hold_cavity  <- (10000 / 2) * 1280 * 0.18

# 5. Consolidated Evaluation Table
resumen <- data.frame(
  Component = c(
    "Magnetron (EOQ)", 
    "Magnetron (Break Tier 3)", 
    "Magnetron (Break Tier 4)", 
    "Transformer WCP (EOQ)", 
    "Cooking Cavity (Break Tier 3)"
  ),
  Order_Qty = c(round(eoq_mag), 2500, 10000, round(eoq_trans), 10000),
  Unit_Price = c(1150, 1130, 1110, 1660, 1280),
  Purchase_Cost = round(c(purch_mag, purch_trans, purch_cavity)),
  Ordering_Cost = round(c(order_mag, order_trans, order_cavity)),
  Holding_Cost  = round(c(hold_mag, hold_trans, hold_cavity)),
  TRC           = round(c(trc_mag_2268, trc_mag_2500, trc_mag_10k, trc_trans_2312, trc_cavity_10k)),
  Orders_Year   = round(c(D / c(round(eoq_mag), 2500, 10000, round(eoq_trans), 10000)), 1),
  Cycle_Days    = round(365 / (D / c(round(eoq_mag), 2500, 10000, round(eoq_trans), 10000)), 1)
)

print(resumen)
##                       Component Order_Qty Unit_Price Purchase_Cost
## 1               Magnetron (EOQ)      2268       1150      81662650
## 2      Magnetron (Break Tier 3)      2500       1130      80242430
## 3      Magnetron (Break Tier 4)     10000       1110      78822210
## 4         Transformer WCP (EOQ)      2312       1660     117878260
## 5 Cooking Cavity (Break Tier 3)     10000       1280      90894080
##   Ordering_Cost Holding_Cost       TRC Orders_Year Cycle_Days
## 1        313100       312984  82288734        31.3       11.7
## 2        284044       339000  80865474        28.4       12.9
## 3         71011      1332000  80225221         7.1       51.4
## 4        460712       460550 118799522        30.7       11.9
## 5         17753      1152000  92063833         7.1       51.4
# 6. Managerial Decision & Annual Savings
savings_mag <- trc_mag_2268 - trc_mag_10k

cat("\n================ MANAGERIAL RECOMMENDATIONS ================\n")
## 
## ================ MANAGERIAL RECOMMENDATIONS ================
cat("1. MAGNETRON:\n")
## 1. MAGNETRON:
cat("   - Optimal Order Quantity (Q*): 10,000 units (Tier 4 Price Break)\n")
##    - Optimal Order Quantity (Q*): 10,000 units (Tier 4 Price Break)
cat("   - Annual Savings vs EOQ: $", format(round(savings_mag), big.mark = ","), "\n")
##    - Annual Savings vs EOQ: $ 2,063,513
cat("   - Operational Cycle: ~7 orders/year (every ~51 days)\n\n")
##    - Operational Cycle: ~7 orders/year (every ~51 days)
cat("2. TRANSFORMER WCP:\n")
## 2. TRANSFORMER WCP:
cat("   - Optimal Order Quantity (Q*):", round(eoq_trans), "units (Feasible EOQ)\n")
##    - Optimal Order Quantity (Q*): 2312 units (Feasible EOQ)
cat("   - Operational Cycle: ~31 orders/year (every ~12 days)\n\n")
##    - Operational Cycle: ~31 orders/year (every ~12 days)
cat("3. COOKING CAVITY:\n")
## 3. COOKING CAVITY:
cat("   - Evaluated Order Quantity: 10,000 units (Tier 3 Price Break)\n")
##    - Evaluated Order Quantity: 10,000 units (Tier 3 Price Break)
cat("   - Total Relevant Cost (TRC): $", format(round(trc_cavity_10k), big.mark = ","), "\n")
##    - Total Relevant Cost (TRC): $ 92,063,833
cat("============================================================\n")
## ============================================================