Ardian About Directory
Claude Code
Email me

Donut Daily Production

2025 — A simulation of how many donuts a bakery should bake each day

01 Problem

A bakery bakes its donuts every morning and sells them by the dozen. Baking too few turns customers away, and baking too many leaves donuts that have to be sold off cheaply at closing.

The goal is the number of dozens to bake each day that earns the most over 5 selling days, rounded to a multiple of 5 dozen as the assignment requires.

02 Customers

The bakery gets 8, 10, 12, or 14 customers a day, most often 8, and 10.2 on average.

cust_per_day_counts <- c(8, 10, 12, 14)
cust_per_day_probs <- c(0.35, 0.30, 0.25, 0.10)

sample_customers <- function() {
sample(cust_per_day_counts, size = 1, prob = cust_per_day_probs)
}

03 Orders

Each customer buys 1 dozen on 40% of visits, 2 on 30%, 3 on 20%, and 4 on 10%, or 2 dozen on average.

Adding up the orders X1,…,XN of a day's N customers gives that day's demand D. It can be anywhere from 8 to 56 dozen, but averages 20.4 and rarely passes 35.

Order sizes do not depend on how many customers come, so the average demand is the average number of customers times the average order. The chance of each demand can be worked out exactly too. For each number of customers n, the chance that n orders add up to d dozen is built one order at a time, then weighted by the chance of n customers.

D=X1+X2+⋯+XN
E[D]=E[N]E[X]=10.2×2=20.4
P(D=d)=∑n=8,10,12,14P(N=n)P(X1+⋯+Xn=d)
dozen_order_per_cust <- c(1, 2, 3, 4)
dozen_order_per_cust_probs <- c(0.40, 0.30, 0.20, 0.10)

sample_orders <- function(n) {
sample(dozen_order_per_cust, size = n, replace = TRUE, prob = dozen_order_per_cust_probs)
}

04 Money

A dozen sells for Rp80,000 and costs Rp55,000 to bake, paid whether it sells or not. Whatever is left at closing goes to small shops nearby for Rp40,000 a dozen.

So every dozen that sells earns Rp25,000, and every dozen left over loses Rp15,000.

Baking q dozen against a demand of D sells min⁡(D,q) and leaves (q−D)+, which is zero once demand reaches the batch. Every dozen baked either sells or is left over. So the day's profit π(q) is Rp25,000 for every dozen baked, minus Rp40,000 for every dozen left over, because a leftover dozen both misses the Rp25,000 it could have earned and loses the Rp15,000 it cost.

π(q)=80,000min⁡(D,q)+40,000(q−D)+−55,000q
π(q)=25,000min⁡(D,q)−15,000(q−D)+
π(q)=25,000q−40,000(q−D)+
simulate_day <- function(dozen_count,
price = 80000,
cost = 55000,
salvage = 40000) {
n_cust <- sample_customers()
demand <- sum(sample_orders(n_cust))
sold <- min(demand, dozen_count)
leftover <- max(dozen_count - demand, 0)
revenue <- price * sold + salvage * leftover
profit <- revenue - cost * dozen_count

data.frame(
demand_dozens = demand,
sold_dozens = sold,
leftover_dozens = leftover,
profit = profit
)
}

05 Simulation

A trial bakes the same number of dozens for 5 days and adds up the profit. Every level from 5 to 100 dozen, in steps of 5, gets 100 trials, the original app's defaults.

The level with the highest mean profit is recommended, and a tie goes to the smaller batch.

evaluate_dozens <- function(dozen_count, days = 5, trials = 200) {
profits <- numeric(trials)
left_avg <- numeric(trials)
for (r in 1:trials) {
df <- simulate_days(dozen_count, days)
profits[r] <- sum(df$profit)
left_avg[r] <- mean(df$leftover_dozens)
}
data.frame(
dozen_count = dozen_count,
mean_profit = mean(profits),
mean_avg_leftover = mean(left_avg)
)
}

dozens_grid <- seq(5, input$max_dozens, by = 5)
res <- do.call(rbind, lapply(dozens_grid, function(dozens) {
evaluate_dozens(dozens, days = input$days, trials = input$trials)
}))
res[order(-res$mean_profit, res$dozen_count), ][1, , drop = FALSE]

06 Answer

Baking 20 dozen a day earns the most, Rp2,135,200 over 5 days on average, with 1.8 dozen left over a day. Past 40 dozen almost nothing extra sells, so every 5 more dozen a day lose Rp375,000 over 5 days.

The expected profit of every batch size can also be added up exactly from the demand distribution. Only the leftover in π(q) is random, so the expected profit needs just the expected leftover, and since every day is alike, 5 days earn 5 times as much as one.

Zoomed in on 15 to 30 dozen, the exact curve peaks at 22 dozen, and among multiples of 5 it gives Rp2,127,214 for 20 dozen and Rp2,083,875 for 25, only Rp43,339 apart.

In this run the simulated means sit up to Rp14,075 off the exact curve. With 100 trials, the standard error of the gap between the 20 and 25 dozen means is Rp43,355, about the size of the margin itself, so this run picks the right level but a rerun can pick 25.

E[π(q)]=25,000q−40,000E[(q−D)+]
E[(q−D)+]=∑d<q(q−d)P(D=d)
5E[π(20)]=5×(25,000×20−40,000×1.8639)≈Rp2,127,214
5E[π(25)]=5×(25,000×25−40,000×5.2056)≈Rp2,083,875

07 Margin

The exact answer has a simple reason. Baking dozen number q on top of the first q−1 changes only that one dozen. It sells when demand reaches q, earning Rp25,000, and is left over otherwise, losing Rp15,000, so it is worth baking as long as it sells on more than 37.5% of days. This is the newsvendor model, the textbook problem of how much to stock of something that cannot be sold the next day.

Each extra dozen sells no more often than the one before it, so the profit climbs while the chance it sells, P(D≥q), stays above 0.375 and falls after. Dozen number 22 sells on 39.0% of days and dozen number 23 on only 32.6%, so the best batch of any size is 22 dozen.

Among multiples of 5, five more dozen are worth baking when they sell on more than 37.5% of days on average. Dozens 16 to 20 sell on 67.8% of days on average and dozens 21 to 25 on only 33.2%, so 20 is the best multiple of 5.

E[π(q)]−E[π(q−1)]=25,000P(D≥q)−15,000P(D<q)
P(D≥q)>15,00025,000+15,000=0.375
P(D≥22)=0.390>0.375
P(D≥23)=0.326<0.375
15∑q=1620P(D≥q)=0.678>0.375
15∑q=2125P(D≥q)=0.332<0.375

08 Original app

The simulation was first built in R as an interactive Shiny dashboard. Sliders set the largest batch to test, up to 200 dozen, and the trials per level, from 50 to 1,000, and inputs change the days, price, cost, and salvage price. Each run draws the profit curve, marks the recommended level, and lists the averages for every level in a table.

It is still live on shinyapps.io. The free tier puts the app to sleep when nobody is using it, so the first visit can take a while to load.

The original Shiny dashboard after 100 trials per level

09 Limits

Rounding to a multiple of 5 dozen has a price. Baking 22 dozen would earn Rp2,146,843 over 5 days, Rp19,629 more than 20.

With 100 trials per level, and fresh random days for every level, the noise in each mean is larger than the gap between 20 and 25 dozen. Rerunning the whole app 2,000 times recommends 25 dozen 14.4% of the time. More trials, or the same random days for every level, would settle it.

The 5-day horizon does not change the answer. Leftovers do not carry over, so every day is the same decision and the 5-day profit is 5 times the daily one. The value box labeled mean 5-day leftover in fact shows the mean leftover per day.

The probabilities come from the assignment, not from sales records, and every day of the week is assumed alike. Real sales logs from the bakery would make the answer worth trusting.

Donut Daily Production

Bakes 20 dozen a day for Rp2,135,200 over 5 days

Donut Daily Production

System characteristics and assumptions

Baking
Once each morning, the same batch every day of a trial
Demand
A random number of customers, each ordering a random number of dozens
Leftovers
Sold off to nearby shops at closing, never carried over
Days
All alike, probabilities assumed, not from sales records

Parameters

Customers a day
8 35%, 10 30%, 12 25%, 14 10%
Dozens per customer
1 40%, 2 30%, 3 20%, 4 10%
Price
Rp80,000 a dozen, Rp40,000 if left over
Cost
Rp55,000 a dozen baked
Trial
5 selling days
Batches
Up to 50 dozen a day

Tests only batches in multiples of 5 dozen, as the assignment requires.

Result

Breakdown