This project answers the two required questions: how much cash may be withdrawn from four ATMs in May 2010, and how much residential electricity may be used in each month of 2014.
I first import the spreadsheet, convert dates and cash amounts, and
remove rows that do not identify an ATM. The Cash values
are in hundreds of dollars, so I simply multiply by 100
when reporting US dollars.
raw_atm <- read_excel("ATM624Data.xlsx", sheet="ATM Data")
stopifnot(all(c("DATE", "ATM", "Cash") %in% names(raw_atm)))
if (inherits(raw_atm$DATE, "Date") || inherits(raw_atm$DATE, "POSIXt")) {
raw_atm$DATE <- as.Date(raw_atm$DATE)
} else if (is.numeric(raw_atm$DATE)) {
raw_atm$DATE <- as.Date(raw_atm$DATE, origin="1899-12-30")
} else {
raw_atm$DATE <- as.Date(raw_atm$DATE)
}
raw_atm$ATM <- as.character(raw_atm$ATM)
raw_atm$Cash <- as.numeric(raw_atm$Cash)
atm <- raw_atm[!is.na(raw_atm$ATM) &
raw_atm$ATM %in% paste0("ATM", 1:4), ]
atm <- as.data.frame(atm[order(atm$ATM, atm$DATE), ])
row.names(atm) <- NULL
stopifnot(nrow(atm) == 1460, !anyDuplicated(atm[c("DATE", "ATM")]))
cat("Historical rows:", nrow(atm), "\n")
## Historical rows: 1460
cat("Excluded unlabeled rows:", nrow(raw_atm) - nrow(atm), "\n")
## Excluded unlabeled rows: 14
cat("Dates:", as.character(min(atm$DATE)), "to",
as.character(max(atm$DATE)), "\n")
## Dates: 2009-05-01 to 2010-04-30
For each ATM, I count missing and zero withdrawals and calculate a few descriptive statistics. This helps avoid treating all four machines as if they behave the same way.
atm_summary <- do.call(rbind, lapply(split(atm, atm$ATM), function(d) {
data.frame(ATM=d$ATM[1], Days=nrow(d),
Missing=sum(is.na(d$Cash)),
Zero_Days=sum(d$Cash == 0, na.rm=TRUE),
Mean_USD=mean(d$Cash, na.rm=TRUE) * 100,
Median_USD=median(d$Cash, na.rm=TRUE) * 100,
Max_USD=max(d$Cash, na.rm=TRUE) * 100)
}))
row.names(atm_summary) <- NULL
kable(atm_summary, digits=0, row.names=FALSE,
caption="Historical daily withdrawals by ATM (dollars)")
| ATM | Days | Missing | Zero_Days | Mean_USD | Median_USD | Max_USD |
|---|---|---|---|---|---|---|
| ATM1 | 365 | 3 | 0 | 8389 | 9100 | 18000 |
| ATM2 | 365 | 2 | 2 | 6258 | 6700 | 14700 |
| ATM3 | 365 | 0 | 362 | 72 | 0 | 9600 |
| ATM4 | 365 | 0 | 0 | 47404 | 40384 | 1091976 |
The source data include five missing withdrawal values for ATM1 and ATM2, 362 zero days for ATM3, and an exceptionally large ATM4 withdrawal on February 9, 2010. ATM3 has only three positive values, all of which match ATM1 on the same dates. The available data do not provide enough information to determine whether ATM3 was activated or whether these entries are incorrect.
Historical chart. I plot daily withdrawals in dollars. Each ATM has its own vertical scale because their withdrawal amounts differ significantly.
atm$Cash_USD <- atm$Cash * 100
ggplot(atm, aes(x=DATE, y=Cash_USD)) +
geom_line(linewidth=0.35, na.rm=TRUE) +
facet_wrap(~ATM, scales="free_y", ncol=2) +
labs(title="Daily ATM withdrawals, May 2009 to April 2010",
x="Date", y="Withdrawals (USD)") +
theme_minimal()
Weekday chart. The chart compares typical withdrawals for each weekday over the most recent four weeks. ATM3 is excluded because its typical withdrawal is zero and its operational status cannot be confirmed from the available data.
recent_atm <- atm[atm$DATE >= as.Date("2010-04-03") &
atm$DATE <= as.Date("2010-04-30") &
atm$ATM != "ATM3", ]
recent_atm$Weekday <- factor(weekday_number(recent_atm$DATE),
levels=1:7, labels=c("Mon", "Tue", "Wed",
"Thu", "Fri", "Sat", "Sun"))
weekday_typical <- aggregate(Cash_USD ~ ATM + Weekday,
data=recent_atm, FUN=median)
ggplot(weekday_typical, aes(x=Weekday, y=Cash_USD, group=ATM)) +
geom_line() + geom_point() +
facet_wrap(~ATM, scales="free_y", ncol=1) +
scale_y_continuous(limits=c(0, NA)) +
labs(title="Recent median withdrawals by weekday",
subtitle="Four weeks ending April 30, 2010",
x="Weekday", y="Median withdrawals (USD)") +
theme_minimal()
I consider five methods: repeat the last day, repeat the last seven days, use the mean of each weekday over the last four weeks, use the median over those four weeks, or use the mean for each weekday over the entire history. The median is less sensitive to extreme values. I do not use ARIMA or more complex methods because these simpler methods are easier to explain and can be compared directly on past dates.
Prediction function. This function uses only data up to a chosen cutoff date and predicts the requested future dates. Keeping the cutoff explicit prevents future information from being used during backtesting.
atm_methods <- c("last_day", "last_week", "weekday_mean_4w",
"weekday_median_4w", "weekday_mean_all")
predict_atm <- function(history, cutoff, dates, method) {
stopifnot(method %in% atm_methods)
d <- history[history$DATE <= cutoff & !is.na(history$Cash), ]
stopifnot(nrow(d) > 0)
if (method == "last_day") {
return(rep(tail(d$Cash, 1), length(dates)))
}
if (method == "last_week") {
last_week_dates <- seq(cutoff - 6, cutoff, by="day")
last_week_values <- d$Cash[match(last_week_dates, d$DATE)]
stopifnot(!anyNA(last_week_values))
return(rep(last_week_values, length.out=length(dates)))
}
if (method != "weekday_mean_all") {
d <- d[d$DATE > cutoff - 28, ]
}
target_weekdays <- weekday_number(dates)
sapply(seq_along(dates), function(i) {
same_weekday <- d$Cash[weekday_number(d$DATE) == target_weekdays[i]]
stopifnot(length(same_weekday) > 0)
if (method == "weekday_median_4w") median(same_weekday)
else mean(same_weekday)
})
}
Backtesting. I simulate forecasts for February, March, and April 2010, using only observations available before each month. I compare models using mean absolute error (MAE), which measures average daily forecasting error. For selecting the May model, I focus on the more recent March–April results.
atm_test_months <- as.Date(c("2010-02-01", "2010-03-01", "2010-04-01"))
atm_errors <- list()
k <- 1
for (a in c("ATM1", "ATM2", "ATM4")) {
d <- atm[atm$ATM == a, ]
# Index the Date vector so R keeps the Date class inside the loop.
for (j in seq_along(atm_test_months)) {
start_day <- atm_test_months[j]
end_day <- seq(start_day, by="month", length.out=2)[2] - 1
days <- seq(start_day, end_day, by="day")
actual <- d$Cash[match(days, d$DATE)]
stopifnot(all(is.finite(actual)))
for (method in atm_methods) {
forecast <- predict_atm(d, start_day - 1, days, method)
atm_errors[[k]] <- data.frame(
ATM=a, Month=format(start_day, "%Y-%m"), Method=method,
Abs_Error=abs(actual - forecast))
k <- k + 1
}
}
}
atm_backtest <- do.call(rbind, atm_errors)
recent_errors <- atm_backtest[atm_backtest$Month %in% c("2010-03", "2010-04"), ]
atm_scores <- aggregate(Abs_Error ~ ATM + Method, recent_errors, mean)
names(atm_scores)[3] <- "MAE_Hundreds"
atm_scores <- atm_scores[order(atm_scores$ATM, atm_scores$MAE_Hundreds), ]
row.names(atm_scores) <- NULL
kable(atm_scores, digits=2, row.names=FALSE,
caption="March–April validation error (hundreds of dollars per day)")
| ATM | Method | MAE_Hundreds |
|---|---|---|
| ATM1 | last_week | 16.02 |
| ATM1 | weekday_mean_4w | 17.47 |
| ATM1 | weekday_median_4w | 20.35 |
| ATM1 | weekday_mean_all | 34.76 |
| ATM1 | last_day | 62.11 |
| ATM2 | last_week | 15.18 |
| ATM2 | weekday_mean_4w | 25.36 |
| ATM2 | weekday_median_4w | 26.65 |
| ATM2 | last_day | 41.49 |
| ATM2 | weekday_mean_all | 52.75 |
| ATM4 | weekday_median_4w | 241.63 |
| ATM4 | last_week | 296.59 |
| ATM4 | weekday_mean_all | 319.52 |
| ATM4 | weekday_mean_4w | 443.43 |
| ATM4 | last_day | 554.56 |
ATM1 and ATM2 have the lowest recent MAE with the seven-day method, while ATM4 does best with the recent weekday median. These models are simple, but they reflect the different behavior of the machines.
Daily forecasts. The selected forecasting methods are used to estimate withdrawals for all 31 days of May for ATM1, ATM2, and ATM4. ATM3 is handled separately: the conditional active scenario uses the mean of its three nonzero readings, while the inactive scenario assumes zero withdrawals.
may_dates <- seq(as.Date("2010-05-01"), as.Date("2010-05-31"), by="day")
chosen_atm <- c(ATM1="last_week", ATM2="last_week",
ATM4="weekday_median_4w")
atm_forecast_parts <- lapply(names(chosen_atm), function(a) {
d <- atm[atm$ATM == a, ]
predicted <- predict_atm(d, as.Date("2010-04-30"),
may_dates, chosen_atm[[a]])
data.frame(Date=may_dates, ATM=a, Method=chosen_atm[[a]],
Forecast_USD=predicted * 100)
})
atm3 <- atm[atm$ATM == "ATM3" & atm$Cash > 0 & !is.na(atm$Cash), ]
stopifnot(nrow(atm3) == 3)
atm_forecast_parts[[4]] <- data.frame(
Date=may_dates, ATM="ATM3", Method="conditional_active_scenario",
Forecast_USD=mean(atm3$Cash) * 100)
atm_daily <- do.call(rbind, atm_forecast_parts)
atm_daily <- atm_daily[order(atm_daily$Date, atm_daily$ATM), ]
row.names(atm_daily) <- NULL
atm_monthly <- aggregate(Forecast_USD ~ ATM, atm_daily, sum)
atm_monthly$Method <- sapply(atm_monthly$ATM, function(a) {
unique(atm_daily$Method[atm_daily$ATM == a])[1]
})
kable(atm_monthly, digits=2, row.names=FALSE,
caption="May 2010 estimated withdrawals by ATM")
| ATM | Forecast_USD | Method |
|---|---|---|
| ATM1 | 240800.0 | last_week |
| ATM2 | 195700.0 | last_week |
| ATM3 | 271766.7 | conditional_active_scenario |
| ATM4 | 1162200.2 | weekday_median_4w |
cat("Total with ATM3 active:", usd(sum(atm_daily$Forecast_USD)), "\n")
## Total with ATM3 active: $1,870,467
cat("Total if ATM3 is inactive:",
usd(sum(atm_daily$Forecast_USD[atm_daily$ATM != "ATM3"])), "\n")
## Total if ATM3 is inactive: $1,598,700
Forecast chart. This plot shows the estimated daily withdrawals. ATM3 is a conditional scenario, not a forecast supported by a long record of active operation.
ggplot(atm_daily, aes(x=Date, y=Forecast_USD)) +
geom_line(linewidth=0.7) +
facet_wrap(~ATM, scales="free_y", ncol=2) +
labs(title="Estimated ATM withdrawals during May 2010",
subtitle="ATM3 assumes the machine is active throughout May",
x="Date", y="Forecast withdrawals (USD)") +
theme_minimal()
ATM1 is forecast to withdraw $240,800 in May 2010, ATM2 $195,700, and ATM4 approximately $1,162,200. ATM3 adds approximately $271,767 only if the machine was genuinely active; the combined conditional estimate is $1,870,467, compared with $1,598,700 if ATM3 was inactive.
The February 9 withdrawal for ATM4 was approximately $1,091,976 in one day. The selected recent median model is less influenced by such unusual transactions, but its forecast is not a recommended cash load. Replenishment planning must also consider existing cash, refill schedules, and the cost of running out. ATM3’s status and the ATM4 spike should be checked in the original transaction records. Only one year of ATM history is available, so I cannot reliably estimate a recurring annual May effect.
Import and dates. The dataset contains 192 monthly
observations from January 1998 through December 2013. The
YYYY-MMM column is used to create the dates, while
CaseSequence is treated as an identifier rather than a time
variable. The KWH column represents monthly electricity
consumption.
power_raw <- read_excel("ResidentialCustomerForecastLoad-624.xlsx",
sheet="ResidentialCustomerForecastLoad")
stopifnot(all(c("CaseSequence", "YYYY-MMM", "KWH") %in% names(power_raw)))
power <- data.frame(Period=as.character(power_raw[["YYYY-MMM"]]),
KWH=as.numeric(power_raw[["KWH"]]))
year <- as.integer(substr(power$Period, 1, 4))
month <- match(substr(power$Period, 6, 8), month.abb)
stopifnot(!anyNA(year), !anyNA(month))
power$Date <- as.Date(sprintf("%04d-%02d-01", year, month))
power$Month <- month
stopifnot(nrow(power) == 192,
identical(power$Date, seq(as.Date("1998-01-01"),
as.Date("2013-12-01"), by="month")))
cat("Months:", nrow(power), "\n")
## Months: 192
cat("Missing KWH:", sum(is.na(power$KWH)), "\n")
## Missing KWH: 1
The source has one missing value (September 2008) and one unusually low value (July 2010: 770,523 kWh). I leave the original spreadsheet unchanged, and will compare results with July 2010 included and with it excluded from regression fitting only.
Historical chart. This chart shows how monthly electricity use changes over the 16 years. The low July 2010 observation is marked in orange.
ggplot(power, aes(x=Date, y=KWH / 1e6)) +
geom_line(colour="#17647A", linewidth=0.5, na.rm=TRUE) +
geom_point(data=power[power$Date == as.Date("2010-07-01"), ],
colour="#C15F3C", size=2.5) +
labs(title="Residential electricity use, 1998–2013",
subtitle="Orange point: unusually low July 2010 reading",
x="Date", y="Monthly consumption (million kWh)") +
theme_minimal()
Month-by-month averages. To investigate seasonality, I calculate the average for each calendar month during 2009–2013.
recent_power <- power[power$Date >= as.Date("2009-01-01"), ]
month_average <- sapply(1:12, function(m) {
mean(recent_power$KWH[recent_power$Month == m], na.rm=TRUE)
})
stopifnot(length(month_average) == 12, all(is.finite(month_average)))
month_profile <- data.frame(
Month=factor(month.abb, levels=month.abb),
Average_KWH=month_average)
kable(month_profile, digits=0, row.names=FALSE,
caption="Average monthly consumption, 2009–2013 (kWh)")
| Month | Average_KWH |
|---|---|
| Jan | 9102286 |
| Feb | 7979908 |
| Mar | 6454149 |
| Apr | 5733632 |
| May | 5482822 |
| Jun | 7277217 |
| Jul | 7175860 |
| Aug | 9054789 |
| Sep | 7974717 |
| Oct | 5676426 |
| Nov | 5559149 |
| Dec | 7546321 |
cat("September average:", format(month_average[9], big.mark=","), "kWh\n")
## September average: 7,974,717 kWh
ggplot(month_profile, aes(x=Month, y=Average_KWH / 1e6)) +
geom_col(fill="darkslategray") +
scale_x_discrete(drop=FALSE) +
labs(title="Average electricity use by month, 2009–2013",
subtitle="All twelve months shown; July 2010 remains in this descriptive average",
x="Month", y="Average consumption (million kWh)") +
theme_minimal()
The data show a repeating seasonal pattern, with relatively high demand in winter and late summer and lower consumption in spring and autumn. The overall level is also generally higher toward the end of the series.
Five forecasting methods are compared: the corresponding month from the previous year (seasonal naïve), the same-month average from the previous three years, and seasonal linear regression using either the full history or the most recent eight years. The final regression method excludes July 2010 during model fitting. More complex methods, such as ARIMA, ETS, and machine learning, are not included because their performance has not been evaluated.
Prediction function. The regression model predicts
electricity consumption using a time index (t) and a month
factor (MonthFactor). The time index captures long-term
changes, while the month factor accounts for seasonal differences in
electricity consumption.
power_methods <- c("seasonal_naive", "monthly_mean_3y",
"regression_all", "regression_8y",
"regression_8y_screened")
predict_power <- function(history, method) {
stopifnot(method %in% power_methods, nrow(history) %% 12 == 0)
if (method == "seasonal_naive") {
return(as.numeric(tail(history$KWH, 12)))
}
if (method == "monthly_mean_3y") {
d <- tail(history, 36)
return(sapply(1:12, function(m) mean(d$KWH[d$Month == m], na.rm=TRUE)))
}
if (method == "regression_all") d <- history
else d <- tail(history, 96) # latest eight years = 96 months
d$t <- seq_len(nrow(d))
d$MonthFactor <- factor(d$Month, levels=1:12)
if (method == "regression_8y_screened") {
d$KWH[d$Date == as.Date("2010-07-01")] <- NA_real_
}
model <- lm(KWH ~ t + MonthFactor, data=d, na.action=na.exclude)
future <- data.frame(t=nrow(d) + 1:12,
MonthFactor=factor(1:12, levels=1:12))
as.numeric(predict(model, newdata=future))
}
Backtesting. For each test year (2011, 2012, and 2013), I make twelve predictions using data only through the preceding December. Then I calculate MAE, RMSE, and MAPE.
power_test_rows <- list()
k <- 1
for (test_year in 2011:2013) {
history <- power[power$Date < as.Date(paste0(test_year, "-01-01")), ]
actual <- power$KWH[format(power$Date, "%Y") == as.character(test_year)]
for (method in power_methods) {
predicted <- predict_power(history, method)
stopifnot(length(predicted) == 12, all(is.finite(predicted)))
power_test_rows[[k]] <- data.frame(
Year=test_year, Month=1:12, Method=method,
Actual_KWH=actual, Forecast_KWH=predicted,
Error_KWH=actual - predicted)
k <- k + 1
}
}
power_backtest <- do.call(rbind, power_test_rows)
power_scores <- do.call(rbind, lapply(power_methods, function(m) {
d <- power_backtest[power_backtest$Method == m, ]
data.frame(Method=m,
MAE=mean(abs(d$Error_KWH)),
RMSE=sqrt(mean(d$Error_KWH^2)),
MAPE=100 * mean(abs(d$Error_KWH / d$Actual_KWH)))
}))
power_scores <- power_scores[order(power_scores$MAE), ]
row.names(power_scores) <- NULL
kable(power_scores, digits=2, row.names=FALSE,
caption="Forecast error across 2011–2013 (36 months)")
| Method | MAE | RMSE | MAPE |
|---|---|---|---|
| regression_8y_screened | 694956.8 | 937071.8 | 8.71 |
| regression_8y | 787626.9 | 1100585.3 | 9.66 |
| regression_all | 846064.1 | 1160845.4 | 10.27 |
| monthly_mean_3y | 868982.0 | 1317366.5 | 10.39 |
| seasonal_naive | 999090.7 | 1868538.4 | 11.84 |
best_power_method <- power_scores$Method[1]
cat("Selected method:", best_power_method, "\n")
## Selected method: regression_8y_screened
The best result is the eight-year seasonal regression with July 2010 omitted from model fitting. Its MAE is about 694,957 kWh, compared with about 999,091 kWh for seasonal naïve forecasting. This only establishes that it performed better in the three historical test years, not that it is guaranteed to be best in 2014.
Monthly forecast. I fit the selected method to all the observations available through December 2013 and forecast the next twelve months. The full-precision numbers are retained for Excel, the report rounds them for readability.
power_forecast <- data.frame(
Date=seq(as.Date("2014-01-01"), by="month", length.out=12),
Month=month.abb,
Forecast_KWH=predict_power(power, best_power_method),
Model=best_power_method)
stopifnot(nrow(power_forecast) == 12,
all(is.finite(power_forecast$Forecast_KWH)))
kable(power_forecast[c("Date", "Forecast_KWH")], digits=0,
row.names=FALSE, caption="Forecast electricity consumption in 2014 (kWh)")
| Date | Forecast_KWH |
|---|---|
| 2014-01-01 | 9616418 |
| 2014-02-01 | 8621356 |
| 2014-03-01 | 7281282 |
| 2014-04-01 | 6468681 |
| 2014-05-01 | 6157129 |
| 2014-06-01 | 7830051 |
| 2014-07-01 | 9129661 |
| 2014-08-01 | 9578787 |
| 2014-09-01 | 8742292 |
| 2014-10-01 | 6500708 |
| 2014-11-01 | 6168474 |
| 2014-12-01 | 8009852 |
actual_2013 <- sum(power$KWH[format(power$Date, "%Y") == "2013"])
forecast_2014 <- sum(power_forecast$Forecast_KWH)
cat("2013 actual:", format(actual_2013, big.mark=","), "kWh\n")
## 2013 actual: 91,420,024 kWh
cat("2014 forecast:", format(round(forecast_2014), big.mark=","), "kWh\n")
## 2014 forecast: 94,104,693 kWh
cat("Change:", round(100 * (forecast_2014 / actual_2013 - 1), 2), "%\n")
## Change: 2.94 %
Forecast chart.The chart shows actual electricity consumption from 2011 to 2013 in blue and the forecasted monthly consumption for 2014 in orange.
recent_actual <- power[power$Date >= as.Date("2011-01-01"), ]
ggplot() +
geom_line(data=recent_actual, aes(x=Date, y=KWH / 1e6),
colour="#17647A", linewidth=0.7) +
geom_line(data=power_forecast, aes(x=Date, y=Forecast_KWH / 1e6),
colour="#C15F3C", linewidth=0.7) +
geom_vline(xintercept=as.numeric(as.Date("2014-01-01")),
linetype="dashed") +
labs(title="Residential power: actual 2011–2013 and forecast 2014",
subtitle="Blue: actual | Orange: forecast",
x="Month", y="Consumption (million kWh)") +
theme_minimal()
## Warning in scale_x_date(): A <numeric> value was passed to a Date scale.
## ℹ The value was converted to a <Date> object.
Excel output. A single workbook is created containing the ATM daily and monthly forecasts, the electricity consumption forecast, and the validation tables.
atm_for_excel <- atm_daily
atm_for_excel$Date <- as.character(atm_for_excel$Date)
power_for_excel <- power_forecast
power_for_excel$Date <- as.character(power_for_excel$Date)
write_xlsx(list(
"ATM Daily May 2010"=atm_for_excel,
"ATM Monthly Summary"=atm_monthly,
"ATM Model Tests"=atm_scores,
"Power Forecast 2014"=power_for_excel,
"Power Model Tests"=power_scores,
"Power Historical Tests"=power_backtest
), path="Part_A_B_Forecasts_from_R.xlsx")
cat("Created: Part_A_B_Forecasts_from_R.xlsx\n")
## Created: Part_A_B_Forecasts_from_R.xlsx
The selected regression model forecasts 94,104,693 kWh of electricity consumption in 2014, approximately 2.94% higher than the 2013 total of 91,420,024 kWh. The historical data show clear monthly seasonal patterns and a general increase in consumption over time. The September 2008 observation is missing, while July 2010 has an unusually low value. July 2010 is excluded from model fitting but remains unchanged in the original dataset. Historical backtesting supports this decision for the evaluation period.
These forecasts are estimates and do not guarantee actual electricity consumption. Factors such as temperature, extreme weather, and customer counts could affect demand but are not included in the available data. Comparing actual monthly consumption in 2014 with the forecasts could help improve future predictions.