PROJECT 1: DATA 624

Author

PASCAL HERMANN KOUOGANG TAFO

PART A: ATM Forecast, ATM624Data.xlsx

1) Installation and Loading libraries

We will first install the packages and load the libraries that would be usefull for our project. I will install the packages in the console so that they are not being reinstall every time that i run my file.

library(fpp3)
Warning: package 'fpp3' was built under R version 4.5.3
── Attaching packages ──────────────────────────────────────────── fpp3 1.0.3 ──
✔ tibble      3.3.1     ✔ tsibble     1.2.0
✔ dplyr       1.2.1     ✔ tsibbledata 0.4.1
✔ tidyr       1.3.2     ✔ ggtime      1.0.0
✔ lubridate   1.9.5     ✔ feasts      0.5.0
✔ ggplot2     4.0.3     ✔ fable       0.5.0
Warning: package 'tibble' was built under R version 4.5.2
Warning: package 'dplyr' was built under R version 4.5.3
Warning: package 'tidyr' was built under R version 4.5.2
Warning: package 'lubridate' was built under R version 4.5.2
Warning: package 'ggplot2' was built under R version 4.5.3
Warning: package 'tsibble' was built under R version 4.5.3
Warning: package 'tsibbledata' was built under R version 4.5.3
Warning: package 'ggtime' was built under R version 4.5.3
Warning: package 'feasts' was built under R version 4.5.3
Warning: package 'fabletools' was built under R version 4.5.3
Warning: package 'fable' was built under R version 4.5.3
── Conflicts ───────────────────────────────────────────────── fpp3_conflicts ──
✖ lubridate::date()    masks base::date()
✖ dplyr::filter()      masks stats::filter()
✖ tsibble::intersect() masks base::intersect()
✖ tsibble::interval()  masks lubridate::interval()
✖ dplyr::lag()         masks stats::lag()
✖ tsibble::setdiff()   masks base::setdiff()
✖ tsibble::union()     masks base::union()
library(readxl)
Warning: package 'readxl' was built under R version 4.5.3
library(forecast)
Warning: package 'forecast' was built under R version 4.5.3
library(tidyverse)
Warning: package 'tidyverse' was built under R version 4.5.3
Warning: package 'readr' was built under R version 4.5.2
Warning: package 'purrr' was built under R version 4.5.3
Warning: package 'stringr' was built under R version 4.5.2
Warning: package 'forcats' was built under R version 4.5.2
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ forcats 1.0.1     ✔ readr   2.2.0
✔ purrr   1.2.2     ✔ stringr 1.6.0
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter()     masks stats::filter()
✖ tsibble::interval() masks lubridate::interval()
✖ dplyr::lag()        masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
library(tseries)
Warning: package 'tseries' was built under R version 4.5.3
Registered S3 method overwritten by 'quantmod':
  method            from
  as.zoo.data.frame zoo 
library(openxlsx)
Warning: package 'openxlsx' was built under R version 4.5.3
library(writexl)
Warning: package 'writexl' was built under R version 4.5.3

2) IMPORT DATA

Let’s import our ATM excel dataset, check the structure of the dataset for a better understanding of the data types we work to facilitate the cleaning of the data and a general descriptive statistics to have an overall initial statistic of our data.

# Load the raw data

atm_raw <- read_excel("ATM624Data.xlsx")
head(atm_raw)
# A tibble: 6 × 3
   DATE ATM    Cash
  <dbl> <chr> <dbl>
1 39934 ATM1     96
2 39934 ATM2    107
3 39935 ATM1     82
4 39935 ATM2     89
5 39936 ATM1     85
6 39936 ATM2     90
# Let's convert the DATE column to match proper date format in R

atm_raw <- atm_raw |>
  mutate(DATE = as.Date(DATE, origin = "1899-12-30"))


str(atm_raw)
tibble [1,474 × 3] (S3: tbl_df/tbl/data.frame)
 $ DATE: Date[1:1474], format: "2009-05-01" "2009-05-01" ...
 $ ATM : chr [1:1474] "ATM1" "ATM2" "ATM1" "ATM2" ...
 $ Cash: num [1:1474] 96 107 82 89 85 90 90 55 99 79 ...
summary(atm_raw)
      DATE                ATM                 Cash        
 Min.   :2009-05-01   Length:1474        Min.   :    0.0  
 1st Qu.:2009-08-01   Class :character   1st Qu.:    0.5  
 Median :2009-11-01   Mode  :character   Median :   73.0  
 Mean   :2009-10-31                      Mean   :  155.6  
 3rd Qu.:2010-02-01                      3rd Qu.:  114.0  
 Max.   :2010-05-14                      Max.   :10919.8  
                                         NA's   :19       

The starting date in our dataset is 2009-05-01 and it ends in 2010-05-14. The cash columns contains 19 missing dates which we should further when cleaning our data.

Let’s verify, if those missing data are in every individual columns.To perform this operation, we will consider the ATM column as our reference.

missing_by_atm <- atm_raw |>
  group_by(ATM) |>
  summarise_all(~sum(is.na(.)))

missing_by_atm
# A tibble: 5 × 3
  ATM    DATE  Cash
  <chr> <int> <int>
1 ATM1      0     3
2 ATM2      0     2
3 ATM3      0     0
4 ATM4      0     0
5 <NA>      0    14

The output confirms that all missing are part of the Cash column with 3 missing cash value from ATM1, 2 missing cash value from ATM2 and 14 from the ATM categories.

Let’s remove the rows with missing ATM values, convert our dataframe into a time series with DATE as our index and plot the original data to observe the pattern of each key variable

# removing 14 rows 

atm_raw_clean <- atm_raw |>
  filter(!is.na(ATM))

# conversion to time series.

atm_raw_clean_ts <- atm_raw_clean |>
  as_tsibble(index=DATE, key= ATM)

# Plot

atm_raw_clean_ts |>
  autoplot(Cash) +
  labs(title = "CASH WITHDRAWAL IN ATMs over time",
       y="Cash in hundreds of dollars")

Interpreting

This plot exhibit that cash withdrawal from ATM1, ATM2, ATM3 fall mostly withim the same range whereas cash withdrawal from ATM4 is in a higher range with a huge outlier in 2010.

We will perform a case by case study meaning forecasting cash withdrawal amount in each ATM.

3) Splitting each ATM time series

We will filter each individual ATM in the ATM column from the overall clean time series.

# INDIVIDUAL Time series

atm1_ts <- atm_raw_clean_ts |> filter(ATM=="ATM1")
atm2_ts <- atm_raw_clean_ts |> filter(ATM=="ATM2")
atm3_ts <- atm_raw_clean_ts |> filter(ATM=="ATM3")
atm4_ts <- atm_raw_clean_ts |> filter(ATM=="ATM4")

# Individual plot


atm1_ts |> autoplot(Cash)

atm2_ts |> autoplot(Cash)

atm3_ts |> autoplot(Cash)

atm4_ts |> autoplot(Cash)

Observation

This individual plot shows the ATM3 cash withdrawal amount is majority equal to zero except from certain case. Let’s check the non zero value.

# number rows in the ATM3 time series

nrow(atm3_ts)
[1] 365
# View all non-zero rows in ATM3

atm3_non_zero <- atm3_ts |> filter(Cash >0)
atm3_non_zero
# A tsibble: 3 x 3 [1D]
# Key:       ATM [1]
  DATE       ATM    Cash
  <date>     <chr> <dbl>
1 2010-04-28 ATM3     96
2 2010-04-29 ATM3     82
3 2010-04-30 ATM3     85
# Calculate the average cash withdrawal

mean(atm3_non_zero$Cash)
[1] 87.66667

There are three non zeros value out of the 365 rows in the ATM3 time series.

4) Testing and Evaluation of Models

# ATM1: model test Candidate Comparison

## Convert to a clean univariate ts

ts_atm1 <- atm_raw %>%
  filter(ATM == "ATM1") %>%
  arrange(DATE) %>%
  pull(Cash) %>%   # extracts the column as a vector, not a data frame
  ts(frequency = 7)

fit_atm1_ets   <- ets(ts_atm1)

fit_atm1_arima <- auto.arima(ts_atm1, seasonal = TRUE, stepwise = FALSE, approximation = FALSE)

fc_atm1 <- forecast(fit_atm1_arima, h = 31) # h=31 because each day represents 1 observation and May has 31 days 

# ATM2: model test Candidate Comparison 

## Convert to a clean univariate ts

ts_atm2 <- atm_raw %>%
  filter(ATM == "ATM2") %>%
  arrange(DATE) %>%
  pull(Cash) %>%   # extracts the column as a vector, not a data frame
  ts(frequency = 7)

fit_atm2_ets   <- ets(ts_atm2)

fit_atm2_arima <- auto.arima(ts_atm2, seasonal = TRUE, stepwise = FALSE, approximation = FALSE)

fc_atm2 <- forecast(fit_atm2_arima, h = 31)

# ATM3: Structural Shift Modeling because of too many zeros values

mean_active_atm3 <- mean(atm3_non_zero$Cash)

# Active baseline forecast for 31 days of May

fc_atm3_point <- rep(mean_active_atm3, 31)

# ATM4:model test Candidate Comparison

## Convert to a clean univariate ts

ts_atm4 <- atm_raw |>
  filter(ATM == "ATM4") |>
  arrange(DATE) |>
  pull(Cash) |>   # extracts the column as a vector, not a data frame
  ts(frequency = 7)


fit_atm4_ets   <- ets(ts_atm4)
fit_atm4_arima <- auto.arima(ts_atm4, seasonal = FALSE, stepwise = FALSE)
fc_atm4 <- forecast(fit_atm4_arima, h = 31)

5) Assemble Forecast output Table for the month of May 2010

may_calendar_dates <- seq(as.Date("2010-05-01"), as.Date("2010-05-31"), by = "day")

forecast_results <- data.frame(
  Date                 = may_calendar_dates,
  ATM1_Cash_Hundreds   = round(as.numeric(fc_atm1$mean), 2)*100,
  ATM2_Cash_Hundreds   = round(as.numeric(fc_atm2$mean), 2),
  ATM3_Cash_Hundreds   = round(fc_atm3_point, 2),
  ATM4_Cash_Hundreds   = round(as.numeric(fc_atm4$mean), 2)
) |>
  mutate(
    ATM1_Cash_withdraw_USD      = ATM1_Cash_Hundreds * 100,
    ATM2_Cash_withdraw_USD      = ATM2_Cash_Hundreds * 100,
    ATM3_Cash_withdraw_USD      = ATM3_Cash_Hundreds * 100,
    ATM4_Cash_withdraw_USD      = ATM4_Cash_Hundreds * 100,
    Daily_Total_USD    = ATM1_Cash_withdraw_USD + ATM2_Cash_withdraw_USD + ATM3_Cash_withdraw_USD + ATM4_Cash_withdraw_USD
  )

head(forecast_results)
        Date ATM1_Cash_Hundreds ATM2_Cash_Hundreds ATM3_Cash_Hundreds
1 2010-05-01               8706              66.35              87.67
2 2010-05-02              10424              72.70              87.67
3 2010-05-03               7351              13.66              87.67
4 2010-05-04                929               5.05              87.67
5 2010-05-05               9953              97.72              87.67
6 2010-05-06               8047              88.68              87.67
  ATM4_Cash_Hundreds ATM1_Cash_withdraw_USD ATM2_Cash_withdraw_USD
1             474.04                 870600                   6635
2             474.04                1042400                   7270
3             474.04                 735100                   1366
4             474.04                  92900                    505
5             474.04                 995300                   9772
6             474.04                 804700                   8868
  ATM3_Cash_withdraw_USD ATM4_Cash_withdraw_USD Daily_Total_USD
1                   8767                  47404          933406
2                   8767                  47404         1105841
3                   8767                  47404          792637
4                   8767                  47404          149576
5                   8767                  47404         1061243
6                   8767                  47404          869739
tail(forecast_results)
         Date ATM1_Cash_Hundreds ATM2_Cash_Hundreds ATM3_Cash_Hundreds
26 2010-05-26              10035              97.75              87.67
27 2010-05-27               8012              88.66              87.67
28 2010-05-28               8752              65.27              87.67
29 2010-05-29               8823              66.39              87.67
30 2010-05-30              10314              72.71              87.67
31 2010-05-31               7340              13.62              87.67
   ATM4_Cash_Hundreds ATM1_Cash_withdraw_USD ATM2_Cash_withdraw_USD
26             474.04                1003500                   9775
27             474.04                 801200                   8866
28             474.04                 875200                   6527
29             474.04                 882300                   6639
30             474.04                1031400                   7271
31             474.04                 734000                   1362
   ATM3_Cash_withdraw_USD ATM4_Cash_withdraw_USD Daily_Total_USD
26                   8767                  47404         1069446
27                   8767                  47404          866237
28                   8767                  47404          937898
29                   8767                  47404          945110
30                   8767                  47404         1094842
31                   8767                  47404          791533

6) Let’s export the dataframe to Excel

write.xlsx(forecast_results, "ATM_Cash_Withdrawal_May_2010_Forecast.xlsx", overwrite = TRUE)
print("Forecast ATM Cash withdrawal of the month of May completed successfully.")
[1] "Forecast ATM Cash withdrawal of the month of May completed successfully."

PART B: Forecasting Power, ResidentialCustomerForecastLoad-624.xlsx

1) Loading the data and checking the characteristics

# load data 

resid_power<- read_excel("ResidentialCustomerForecastLoad-624.xlsx")
head(resid_power)
# A tibble: 6 × 3
  CaseSequence `YYYY-MMM`     KWH
         <dbl> <chr>        <dbl>
1          733 1998-Jan   6862583
2          734 1998-Feb   5838198
3          735 1998-Mar   5420658
4          736 1998-Apr   5010364
5          737 1998-May   4665377
6          738 1998-Jun   6467147
tail(resid_power)
# A tibble: 6 × 3
  CaseSequence `YYYY-MMM`     KWH
         <dbl> <chr>        <dbl>
1          919 2013-Jul   8415321
2          920 2013-Aug   9080226
3          921 2013-Sep   7968220
4          922 2013-Oct   5759367
5          923 2013-Nov   5769083
6          924 2013-Dec   9606304
# check structure and descriptive stat

str(resid_power)
tibble [192 × 3] (S3: tbl_df/tbl/data.frame)
 $ CaseSequence: num [1:192] 733 734 735 736 737 738 739 740 741 742 ...
 $ YYYY-MMM    : chr [1:192] "1998-Jan" "1998-Feb" "1998-Mar" "1998-Apr" ...
 $ KWH         : num [1:192] 6862583 5838198 5420658 5010364 4665377 ...
dim(resid_power)
[1] 192   3
summary(resid_power)
  CaseSequence     YYYY-MMM              KWH          
 Min.   :733.0   Length:192         Min.   :  770523  
 1st Qu.:780.8   Class :character   1st Qu.: 5429912  
 Median :828.5   Mode  :character   Median : 6283324  
 Mean   :828.5                      Mean   : 6502475  
 3rd Qu.:876.2                      3rd Qu.: 7620524  
 Max.   :924.0                      Max.   :10655730  
                                    NA's   :1         

The dataset is 192 rows and 3 columns dataset and the summary statistic shows that there is one missing residential power usage value. It is a monthly frequenced dataset.

2) Let’s convert the data to the time series and plot it

# The frequency is monthly (12) and the series starts in Jan 1998

ts_resid_power <- ts(resid_power$KWH, start = c(1998, 1), frequency = 12)


ts_resid_power_clean <- tsclean(ts_resid_power)
head(ts_resid_power_clean)
         Jan     Feb     Mar     Apr     May     Jun
1998 6862583 5838198 5420658 5010364 4665377 6467147
tail(ts_resid_power_clean)
         Jul     Aug     Sep     Oct     Nov     Dec
2013 8415321 9080226 7968220 5759367 5769083 6987874

3) Testing and Evaluating models

# Let's test and evaluate Exponential Smoothing / Holt-Winters Models

fit_ses   <- ses(ts_resid_power_clean, h = 12)
fit_holt  <- holt(ts_resid_power_clean, h = 12)
fit_ets_m <- ets(ts_resid_power_clean, model = "MAM")
fit_ets_a <- ets(ts_resid_power_clean, model = "AAA")
fit_ets_auto <- ets(ts_resid_power_clean)

# Let's test and evaluate Seasonal ARIMA Modeling

lambda_PU <- BoxCox.lambda(ts_resid_power_clean)
cat("Optimal Box-Cox Lambda:", lambda_PU, "\n")
Optimal Box-Cox Lambda: -0.1454175 
# let's fit auto.arima with and without Box-Cox transformation

fit_arima_raw <- auto.arima(ts_resid_power_clean, stepwise = FALSE, approximation = FALSE)
fit_arima_bc  <- auto.arima(ts_resid_power_clean, lambda = lambda_PU, stepwise = FALSE, approximation = FALSE)

4} Comparative models evaluation

# Let's check the AICc parameters from the models

cat("\n--- Model Comparison Summary ---\n")

--- Model Comparison Summary ---
cat("ETS Model Selected:", fit_ets_auto$method, "AICc:", fit_ets_auto$aicc, "\n")
ETS Model Selected: ETS(M,A,M) AICc: 6126.687 
cat("Raw ARIMA Selected:", arimaorder(fit_arima_raw), "AICc:", fit_arima_raw$aicc, "\n")
Raw ARIMA Selected: 0 0 3 2 1 0 12 AICc: 5307.867 
cat("Box-Cox ARIMA:", arimaorder(fit_arima_bc), "AICc:", fit_arima_bc$aicc, "\n")
Box-Cox ARIMA: 0 0 3 2 1 0 12 AICc: -1173.288 
# Let's check Accuracy metrics on fitted models

cat("\nIn-Sample Accuracy - ETS:\n")

In-Sample Accuracy - ETS:
print(forecast::accuracy(fit_ets_auto))
                    ME   RMSE      MAE        MPE     MAPE      MASE      ACF1
Training set -7298.943 583970 444014.7 -0.7599925 6.764759 0.7453874 0.3058204
cat("\nIn-Sample Accuracy - ARIMA:\n")

In-Sample Accuracy - ARIMA:
print(forecast::accuracy(fit_arima_bc))
                   ME     RMSE      MAE         MPE     MAPE      MASE
Training set 27171.71 566612.1 433555.9 -0.08069716 6.637629 0.7278298
                   ACF1
Training set 0.05365242
# Let's check residuals diagnostics to choose the best model

checkresiduals(fit_arima_bc)


    Ljung-Box test

data:  Residuals from ARIMA(0,0,3)(2,1,0)[12] with drift
Q* = 11.874, df = 19, p-value = 0.8909

Model df: 5.   Total lags used: 24

Interpretation

The model residuals exhibit a bell shape curve that is centered around zero with a constant variance across time which confirms that the seasonal differencing and Box-Cox transformation successfully stabilized the trend and heteroskedastic seasonal swings. The ACF plot displays all spikes lying within the 95% confidence blue dashed bands, indicating that the remaining variations behave essentially like white noise.

5) Let’s Generate and visualize the 2014 residendtial power usage Forecast with a 12 months observations.

# reporting forecast

fc_2014 <- forecast(fit_arima_bc, h = 12)
fc_2014
         Point Forecast   Lo 80    Hi 80   Lo 95    Hi 95
Jan 2014        9465457 8407975 10678093 7903327 11391560
Feb 2014        8581952 7604389  9706173 7138886 10369106
Mar 2014        6580433 5856220  7409092 5510046  7895815
Apr 2014        5945084 5285857  6700206 4971004  7144112
May 2014        5886529 5234673  6633063 4923294  7071853
Jun 2014        8229114 7275475  9328683 6822251  9978397
Jul 2014        9523516 8397839 10825426 7864078 11596536
Aug 2014       10056917 8859403 11443516 8292071 12265528
Sep 2014        8492852 7504437  9633267 7034915 10307459
Oct 2014        5813572 5170885  6549424 4863830  6981853
Nov 2014        6097026 5418628  6874494 5094741  7331715
Dec 2014        7601354 6729884  8604540 6315203  9196549
# Ploting

plot(fc_2014, main = "Residential KWH Power Usage Forecast for 2014",
     ylab = "Power Consumption (KWH)", xlab = "Time")
grid()

Result

This plot exhibit the 1998–2013 historical power usage alongside the 2014 monthly forecast, which proves that the model successfully reproduces the characteristic bimodal seasonal pattern of peak consumption during summer and winter because they are cooling and heating months.

6) Let’s Add the forecast power usage to the existing files

# Let's generate the 2014 forecast data

months_2014 <- c("2014-Jan", "2014-Feb", "2014-Mar", "2014-Apr", 
                 "2014-May", "2014-Jun", "2014-Jul", "2014-Aug", 
                 "2014-Sep", "2014-Oct", "2014-Nov", "2014-Dec")

df_forecast <- data.frame(
  CaseSequence = seq(from = max(resid_power$CaseSequence) + 1, length.out = 12),
  `YYYY-MMM`   = months_2014,
  KWH          = round(as.numeric(fc_2014$mean), 0),
  check.names  = FALSE
)

# Let's combine original data and new generated forecast data

df_updated <- bind_rows(resid_power
                        , df_forecast)

# Let's Write to fcombined Excel file 

output_file <- "ResidentialCustomerForecastLoad-624_Forecasted.xlsx"
write_xlsx(df_updated, path = output_file)