# training = 2022-2023, test = 2024
# path names
flight.folder  <- "~/Downloads/Flight Punctuality Data UK, 2022-2024"
weather.folder <- "~/Downloads/Weather data"
set.seed(7406)

# 1. read CAA flight files ----------------------------------------------------------------------------
flight.files <- list.files(flight.folder, pattern = "\\.csv$", full.names = TRUE, recursive = TRUE)

flight.list <- list()
for (i in 1:length(flight.files)){
  flight.list[[i]] <- read.csv(file = flight.files[i], sep = ",", header=T);
}
flights <- do.call(rbind, flight.list)
print(dim(flights)) #115754 rows 
## [1] 115754     25
# 2. clean flight data ---------------------------------------------------------------------------------

flights <- flights[
  #keep three target airport
  flights$reporting_airport %in% c("HEATHROW", "GATWICK", "MANCHESTER") & 
    #keep scheduled flights only, removed chartered flights
    flights$scheduled_charter == "S" &
    #remove rows with zero match flights
    !is.na(flights$number_flights_matched) & flights$number_flights_matched > 0, 
]

print(table(flights$reporting_airport))
## 
##    GATWICK   HEATHROW MANCHESTER 
##       9568      11225      10462
# 3. read weather files ---------------------------------------------------------------------------------
weather.files <- list.files(weather.folder, pattern = "\\.csv$", full.names = TRUE, recursive = TRUE)

print(length(weather.files)) #4 files 
## [1] 4
weather.list <- list()
for (i in 1:length(weather.files)){
  weather.list[[i]] <- read.csv(file = weather.files[i], sep = ",", header=T);
}
weather <- do.call(rbind, weather.list)
print(dim(weather)) #3288 rows
## [1] 3288   33
# 4. standardize airport names to match CAA naming------------------------------------------------------ 
weather$airport <- trimws(weather$name)
weather$airport[weather$airport == "London Heathrow Airport, United Kingdom"] <- "HEATHROW"
weather$airport[weather$airport == "London Gatwick Airport, United Kingdom"] <- "GATWICK"
weather$airport[weather$airport == "Manchester Airport, United Kingdom"] <- "MANCHESTER"
weather <- weather[weather$airport %in% c("HEATHROW", "GATWICK", "MANCHESTER"), ]
print(table(weather$airport))
## 
##    GATWICK   HEATHROW MANCHESTER 
##       1096       1096       1096
# 5. create a reporting period matching the CAA data format---------------------------------------------
weather$datetime <- as.Date(weather$datetime,format = "%Y-%m-%d")
weather$reporting_period <- as.numeric(format(weather$datetime, "%Y%m"))

# 6. feature engineering weather variables--------------------------------------------------------------

# calculates monthly averages (airport-month-weather)
monthly.mean <- aggregate(
  cbind(temp, humidity, precip, snow, windgust, windspeed,
        sealevelpressure, cloudcover, visibility) ~ airport + reporting_period,
  data = weather, FUN = mean)

# calculates monthly standard deviations 
# addresses TA's suggestion..new features will help keep info within month weather variation
# selected these variables because their monthly average could hide anomalies; day to day variation within a month could affect operations
# EX. precip - month can have few days of heavy rain; windspeed - month could have calm weather for most of the month but a coupole of high wind days which affect operations
monthly.variability <- aggregate(
  cbind(precip, snow, windspeed, visibility) ~ airport + reporting_period,
  data = weather, FUN = sd)

#rename variables to std dev 
names(monthly.variability)[3:6] <- c("precip_sd", "snow_sd", "windspeed_sd", "visibility_sd")

# calculates max windspeed during each airport month
# addresses TA's concern that monthly averages can hide severe wind days; strong winds could potentially affect operations
monthly.maxwind <- aggregate(
  windspeed ~ airport + reporting_period,
  data = weather, FUN = max)

names(monthly.maxwind)[3] <- "max_windspeed"

#combine all monthly weather features
weather.monthly <- merge(monthly.mean, monthly.variability,
                         by = c("airport", "reporting_period"))
weather.monthly <- merge(weather.monthly, monthly.maxwind,
                         by = c("airport", "reporting_period"))

print(dim(weather.monthly)) #108 rows, 16 cols
## [1] 108  16
print(head(weather.monthly))
##   airport reporting_period     temp humidity     precip snow windgust windspeed
## 1 GATWICK           202201 40.28065 87.38710 0.03338710    0 22.51613  8.935484
## 2 GATWICK           202202 45.24286 80.10357 0.09128571    0 28.11786 15.496429
## 3 GATWICK           202203 46.03871 79.05484 0.04970968    0 21.18710 11.922581
## 4 GATWICK           202204 48.82667 72.83000 0.02213333    0 22.14667 12.553333
## 5 GATWICK           202205 55.88065 77.26452 0.06735484    0 20.04839 10.858065
## 6 GATWICK           202206 60.04000 74.29000 0.02836667    0 21.31667 11.270000
##   sealevelpressure cloudcover visibility  precip_sd snow_sd windspeed_sd
## 1         1025.848   60.05806   10.47419 0.09471208       0     4.134211
## 2         1016.196   65.84286   15.46786 0.15053063       0     5.817183
## 3         1021.823   58.57097   11.51290 0.11369175       0     3.620931
## 4         1015.723   57.15000   17.64667 0.05636013       0     4.636120
## 5         1018.445   65.20323   17.01290 0.14251048       0     3.168993
## 6         1015.913   54.00000   16.48000 0.06356885       0     2.798910
##   visibility_sd max_windspeed
## 1      5.449157          17.5
## 2      5.227354          34.9
## 3      5.357098          21.3
## 4      3.839516          26.5
## 5      5.356351          18.0
## 6      4.215930          17.0
# 7. merge flights with monthly weather by airport + month----------------------------------------------------
flights.merged <- merge(flights, weather.monthly,
                        by.x = c("reporting_airport", "reporting_period"),
                        by.y = c("airport", "reporting_period"))
print(dim(flights.merged)) #31255x39
## [1] 31255    39
# create airport indicator variables and identifier
heathrow = I(flights.merged$reporting_airport == "HEATHROW")
gatwick  = I(flights.merged$reporting_airport == "GATWICK")
#manchester is the reference airport 

# creates airport month identifier
airport_month = interaction(flights.merged$reporting_airport,flights.merged$reporting_period,drop = TRUE)

# combine variables
flightdata = data.frame(heathrow,gatwick,airport_month,flights.merged)

#converts airport indiciators to 0 and 1
flightdata$heathrow = as.numeric(flightdata$heathrow)
flightdata$gatwick  = as.numeric(flightdata$gatwick)

# 8. splits data before creating the response variable------------------------------------------------------

# split before calculating the median
# train = 2022-2023, test = 2024 (time based split)
flight.train = flightdata[flightdata$reporting_period <= 202312,]
flight.test = flightdata[flightdata$reporting_period >= 202401,]

# calculates median average delay threshold from training data (2022-2023) 
# addresses TA'as concerns about data leakage because 2024 values aren't included in the threshold
delay.threshold = median(flight.train$average_delay_mins,na.rm = TRUE)

# binary response: above training median = 1, at or below training median = 0
flight.train$delayed = as.numeric(flight.train$average_delay_mins >delay.threshold)
#same threshold applied to 2024
flight.test$delayed = as.numeric(flight.test$average_delay_mins >delay.threshold)

# define predictors for models
continuous.predictors = c(
  "number_flights_matched",
  "temp",
  "humidity",
  "precip",
  "snow",
  "windspeed",
  "sealevelpressure",
  "cloudcover",
  "visibility",
  "precip_sd",
  "snow_sd",
  "windspeed_sd",
  "visibility_sd",
  "max_windspeed"
)

dummy.predictors = c("heathrow","gatwick")
#combines predictor lists 
predictor.names = c(continuous.predictors,dummy.predictors)

#final list 
required.model = c("delayed","reporting_period","airport_month",predictor.names)

print(nrow(flight.train)) #training observations = 20396
## [1] 20396
print(nrow(flight.test)) #test observations - 10859
## [1] 10859
print(table(flight.train$delayed)) #0 = 10199, 1=10197
## 
##     0     1 
## 10199 10197
print(table(flight.test$delayed)) #0 = 6458, 1 = 4401
## 
##    0    1 
## 6458 4401
# EDA ---------------------------------------------------------------------------------------------------------------------------

#boxplots
par(mfrow = c(3, 3), mar = c(4, 4, 3, 1))

boxplot(number_flights_matched ~ delayed, data = flight.train, main = "Flights Matched", xlab = "Delay Class", ylab = "Number of Flights")
boxplot(temp ~ delayed, data = flight.train, main = "Temperature", xlab = "Delay Class", ylab = "Temperature")
boxplot(humidity ~ delayed, data = flight.train, main = "Humidity", xlab = "Delay Class", ylab = "Humidity")
boxplot(precip ~ delayed, data = flight.train, main = "Precipitation", xlab = "Delay Class", ylab = "Precipitation")
boxplot(snow ~ delayed, data = flight.train, main = "Snow", xlab = "Delay Class", ylab = "Snow")
boxplot(windspeed ~ delayed, data = flight.train, main = "Wind Speed", xlab = "Delay Class", ylab = "Wind Speed")
boxplot(sealevelpressure ~ delayed, data = flight.train, main = "Pressure", xlab = "Delay Class", ylab = "Sea-Level Pressure")
boxplot(cloudcover ~ delayed, data = flight.train, main = "Cloud Cover", xlab = "Delay Class", ylab = "Cloud Cover")
boxplot(visibility ~ delayed, data = flight.train, main = "Visibility", xlab = "Delay Class", ylab = "Visibility")

par(mfrow = c(1, 1))

# 9. Model -------------------------------------------------------------------------------------------------------------------

library(MASS) 
library(e1071) 
library(class) 
library(pROC) 
## Type 'citation("pROC")' for a citation.
## 
## Attaching package: 'pROC'
## The following objects are masked from 'package:stats':
## 
##     cov, smooth, var
library(sandwich) 
library(lmtest) 
## Loading required package: zoo
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
TrainErr <- NULL
TestErr <- NULL

# LDA----------------------------------------------

mod1 <- lda(flight.train[, predictor.names], flight.train$delayed)

pred1 <- predict(mod1, flight.train[, predictor.names])$class
TrainErr <- c(TrainErr, mean(pred1 != flight.train$delayed))

pred1test.output <- predict(mod1, flight.test[, predictor.names])
pred1test <- pred1test.output$class
TestErr <- c(TestErr, mean(pred1test != flight.test$delayed))

tab1 <- table(
  Predicted = factor(pred1test, levels = c(0, 1)),
  Actual = factor(flight.test$delayed, levels = c(0, 1))
)

print(tab1)
##          Actual
## Predicted    0    1
##         0 4569 2268
##         1 1889 2133
# QDA----------------------------------------------

mod2 <- qda(flight.train[, predictor.names], flight.train$delayed)

pred2 <- predict(mod2, flight.train[, predictor.names])$class
TrainErr <- c(TrainErr, mean(pred2 != flight.train$delayed))

pred2test.output <- predict(mod2, flight.test[, predictor.names])
pred2test <- pred2test.output$class
TestErr <- c(TestErr, mean(pred2test != flight.test$delayed))

tab2 <- table(
  Predicted = factor(pred2test, levels = c(0, 1)),
  Actual = factor(flight.test$delayed, levels = c(0, 1))
)

print(tab2)
##          Actual
## Predicted    0    1
##         0 3668 2216
##         1 2790 2185
# NAIVE BAYES----------------------------------------------

nb.train <- flight.train
nb.train$delayed <- factor(nb.train$delayed, levels = c(0, 1))

mod3 <- naiveBayes(nb.train[, predictor.names], nb.train$delayed)

pred3 <- predict(mod3, flight.train[, predictor.names], type = "class")
pred3.numeric <- as.numeric(as.character(pred3))
TrainErr <- c(TrainErr, mean(pred3.numeric != flight.train$delayed))

pred3test <- predict(mod3, flight.test[, predictor.names], type = "class")
pred3test.numeric <- as.numeric(as.character(pred3test))
TestErr <- c(TestErr, mean(pred3test.numeric != flight.test$delayed))

pred3test.prob <- predict(mod3, flight.test[, predictor.names], type = "raw")

tab3 <- table(
  Predicted = factor(pred3test.numeric, levels = c(0, 1)),
  Actual = factor(flight.test$delayed, levels = c(0, 1))
)

print(tab3)
##          Actual
## Predicted    0    1
##         0 2962 1212
##         1 3496 3189
# LOGISTIC REGRESSION----------------------------------------------

model.formula <- as.formula(paste("delayed ~", paste(predictor.names, collapse = " + ")))

mod4 <- glm(model.formula, family = binomial, data = flight.train)
print(summary(mod4))
## 
## Call:
## glm(formula = model.formula, family = binomial, data = flight.train)
## 
## Coefficients:
##                          Estimate Std. Error z value Pr(>|z|)    
## (Intercept)             7.118e+01  5.265e+00  13.521  < 2e-16 ***
## number_flights_matched -5.904e-04  1.767e-04  -3.341 0.000836 ***
## temp                    7.785e-02  4.055e-03  19.199  < 2e-16 ***
## humidity               -5.345e-03  4.638e-03  -1.152 0.249148    
## precip                 -2.984e+00  8.726e-01  -3.419 0.000629 ***
## snow                   -4.447e+01  5.588e+00  -7.957 1.76e-15 ***
## windspeed              -4.189e-02  1.275e-02  -3.285 0.001020 ** 
## sealevelpressure       -7.419e-02  4.937e-03 -15.026  < 2e-16 ***
## cloudcover              5.889e-03  1.304e-03   4.515 6.33e-06 ***
## visibility              3.809e-02  6.542e-03   5.822 5.81e-09 ***
## precip_sd              -2.318e+00  5.016e-01  -4.622 3.81e-06 ***
## snow_sd                 1.111e+01  1.438e+00   7.728 1.09e-14 ***
## windspeed_sd            2.513e-01  3.370e-02   7.456 8.89e-14 ***
## visibility_sd           6.184e-02  1.396e-02   4.429 9.46e-06 ***
## max_windspeed          -4.350e-02  7.705e-03  -5.645 1.65e-08 ***
## heathrow                5.648e-01  8.157e-02   6.924 4.39e-12 ***
## gatwick                 4.775e-01  4.980e-02   9.589  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 28275  on 20395  degrees of freedom
## Residual deviance: 26095  on 20379  degrees of freedom
## AIC: 26129
## 
## Number of Fisher Scoring iterations: 4
pred4.prob <- predict(mod4, flight.train, type = "response")
pred4 <- as.numeric(pred4.prob >= 0.50)
TrainErr <- c(TrainErr, mean(pred4 != flight.train$delayed))

pred4test.prob <- predict(mod4, flight.test, type = "response")
pred4test <- as.numeric(pred4test.prob >= 0.50)
TestErr <- c(TestErr, mean(pred4test != flight.test$delayed))

tab4 <- table(
  Predicted = factor(pred4test, levels = c(0, 1)),
  Actual = factor(flight.test$delayed, levels = c(0, 1))
)

print(tab4)
##          Actual
## Predicted    0    1
##         0 4535 2223
##         1 1923 2178
# KNN TUNING----------------------------------------------
# addresses TA's concerns of data leakage; chooses the best k without using the 2024 test data
# uses earlier observations 2022-2023 to train KNN
# uses later observations 2022-2023 to compare k values 
# selects k w lowest error
# trains finall KNN on all 2022-2023 obs
# evalutes it on 2024 data

# fold 1: train on 2022, validate on Jan-Jun 2023
# fold 2: train on Jan 2022-Jun 2023, validate on Jul-Dec 2023
folds <- data.frame(
  train.end = c(202212, 202306),
  validation.start = c(202301, 202307),
  validation.end = c(202306, 202312)
)

# test values of k from 5 to 75
k.values <- seq(5, 75, by = 5)
knn.validation <- data.frame()

for (f in 1:nrow(folds)) {
  
  # create training and validation datasets
  fold.train <- flight.train[flight.train$reporting_period <= folds$train.end[f], ]
  
  fold.validation <- flight.train[
    flight.train$reporting_period >= folds$validation.start[f] &
      flight.train$reporting_period <= folds$validation.end[f], ]
  
  # calculate scaling values using fold training data only
  train.means <- sapply(fold.train[, continuous.predictors], mean, na.rm = TRUE)
  train.sds <- sapply(fold.train[, continuous.predictors], sd, na.rm = TRUE)
  train.sds[is.na(train.sds) | train.sds == 0] <- 1
  
  # scale predictors and add unscaled airport indicators
  x.train <- cbind(
    scale(fold.train[, continuous.predictors], center = train.means, scale = train.sds), fold.train[, dummy.predictors])
  
  x.validation <- cbind(
    scale(fold.validation[, continuous.predictors], center = train.means, scale = train.sds), fold.validation[, dummy.predictors])
  
  y.train <- factor(fold.train$delayed, levels = c(0, 1))
  
  # test each value of k
  for (k in k.values) {
    prediction <- knn(train = x.train, test = x.validation, cl = y.train, k = k)
    validation.error <- mean(
      as.numeric(as.character(prediction)) != fold.validation$delayed)
      knn.validation <- rbind(knn.validation, data.frame(Fold = f, K = k, ValidationError = validation.error))}}

# average error across two folds
knn.summary <- aggregate(ValidationError ~ K, data = knn.validation, FUN = mean)

# select k with the lowest error
best.k <- knn.summary$K[which.min(knn.summary$ValidationError)]

# plot average error
plot(
  knn.summary$K, knn.summary$ValidationError,
  type = "b",
  xlab = "Number of Neighbors (k)",
  ylab = "Mean Validation Error",
  main = "KNN Time-Based Validation"
)

print(knn.summary)
##     K ValidationError
## 1   5       0.4040346
## 2  10       0.4006973
## 3  15       0.4006388
## 4  20       0.3986138
## 5  25       0.3992907
## 6  30       0.3984178
## 7  35       0.3953143
## 8  40       0.3956366
## 9  45       0.3954518
## 10 50       0.3954481
## 11 55       0.3967302
## 12 60       0.3986590
## 13 65       0.3997526
## 14 70       0.4002108
## 15 75       0.4006690
print(best.k)
## [1] 35
# FINAL KNN MODEL----------------------------------------------

# estimate scaling using all 2022-2023 training observations
train.means <- sapply(flight.train[, continuous.predictors], mean, na.rm = TRUE)
train.sds <- sapply(flight.train[, continuous.predictors], sd, na.rm = TRUE)
train.sds[is.na(train.sds) | train.sds == 0] <- 1

flight.train.scaled <- flight.train
flight.test.scaled <- flight.test

flight.train.scaled[, continuous.predictors] <- scale(
  flight.train[, continuous.predictors],
  center = train.means,
  scale = train.sds
)

flight.test.scaled[, continuous.predictors] <- scale(
  flight.test[, continuous.predictors],
  center = train.means,
  scale = train.sds
)

x.train <- as.matrix(flight.train.scaled[, predictor.names])
x.test <- as.matrix(flight.test.scaled[, predictor.names])
y.train <- factor(flight.train$delayed, levels = c(0, 1))

# KNN training prediction
pred5 <- knn(x.train, x.train, y.train, k = best.k)
pred5.numeric <- as.numeric(as.character(pred5))
TrainErr <- c(TrainErr, mean(pred5.numeric != flight.train$delayed))

# final KNN test prediction
pred5test <- knn(x.train, x.test, y.train, k = best.k, prob = TRUE)
pred5test.numeric <- as.numeric(as.character(pred5test))
TestErr <- c(TestErr, mean(pred5test.numeric != flight.test$delayed))

tab5 <- table(
  Predicted = factor(pred5test.numeric, levels = c(0, 1)),
  Actual = factor(flight.test$delayed, levels = c(0, 1))
)

print(tab5)
##          Actual
## Predicted    0    1
##         0 3889 2072
##         1 2569 2329
# 10. calculate training and testing error--------------------------------------------------------------------------------------

error.results <- data.frame(
  Model = c("LDA", "QDA", "Naive Bayes", "Logistic Regression",
            paste0("KNN (k = ", best.k, ")")),
  TrainingError = TrainErr,
  TestingError = TestErr
)

error.results$TrainingError <- round(error.results$TrainingError, 4)
error.results$TestingError <- round(error.results$TestingError, 4)

print(error.results)
##                 Model TrainingError TestingError
## 1                 LDA        0.3700       0.3828
## 2                 QDA        0.3384       0.4610
## 3         Naive Bayes        0.3759       0.4336
## 4 Logistic Regression        0.3702       0.3818
## 5        KNN (k = 35)        0.3000       0.4274
# 11. majority class benchmark--------------------------------------------------------------------------------------------
#benchmark error = 40.53%
#assigned all 2024 observations to lower delay bc delay class = 0 was more common in 2022-2023

majority.class <- as.numeric(names(which.max(table(flight.train$delayed))))
majority.prediction <- rep(majority.class, nrow(flight.test))
majority.error <- mean(majority.prediction != flight.test$delayed)

cat("Majority-class benchmark error:", round(majority.error, 4), "\n")
## Majority-class benchmark error: 0.4053
tab0 <- table(
  Predicted = factor(majority.prediction, levels = c(0, 1)),
  Actual = factor(flight.test$delayed, levels = c(0, 1))
)

print(tab0)
##          Actual
## Predicted    0    1
##         0 6458 4401
##         1    0    0
#  12. performance metrics-------------------------------------------------------------------------------------------------------------------

model.metrics <- function(tab) {
  TN <- tab["0", "0"]
  FN <- tab["0", "1"]
  FP <- tab["1", "0"]
  TP <- tab["1", "1"]
  
  sensitivity <- TP / (TP + FN)
  specificity <- TN / (TN + FP)
  precision <- ifelse(TP + FP == 0, NA, TP / (TP + FP))
  
  f1 <- ifelse(
    is.na(precision) | precision + sensitivity == 0,
    NA,
    2 * precision * sensitivity / (precision + sensitivity)
  )
  
  accuracy <- (TP + TN) / sum(tab)
  error <- 1 - accuracy
  balanced.accuracy <- mean(c(sensitivity, specificity))
  
  return(c(
    Error = error,
    Accuracy = accuracy,
    Sensitivity = sensitivity,
    Specificity = specificity,
    Precision = precision,
    F1 = f1,
    BalancedAccuracy = balanced.accuracy
  ))
}

results <- rbind(
  model.metrics(tab0),
  model.metrics(tab1),
  model.metrics(tab2),
  model.metrics(tab3),
  model.metrics(tab4),
  model.metrics(tab5)
)

rownames(results) <- c(
  "Majority Benchmark",
  "LDA",
  "QDA",
  "Naive Bayes",
  "Logistic Regression",
  paste0("KNN k=", best.k)
)

print(round(results, 4))
##                      Error Accuracy Sensitivity Specificity Precision     F1
## Majority Benchmark  0.4053   0.5947      0.0000      1.0000        NA     NA
## LDA                 0.3828   0.6172      0.4847      0.7075    0.5303 0.5065
## QDA                 0.4610   0.5390      0.4965      0.5680    0.4392 0.4661
## Naive Bayes         0.4336   0.5664      0.7246      0.4587    0.4770 0.5753
## Logistic Regression 0.3818   0.6182      0.4949      0.7022    0.5311 0.5124
## KNN k=35            0.4274   0.5726      0.5292      0.6022    0.4755 0.5009
##                     BalancedAccuracy
## Majority Benchmark            0.5000
## LDA                           0.5961
## QDA                           0.5322
## Naive Bayes                   0.5916
## Logistic Regression           0.5986
## KNN k=35                      0.5657
# 12. confusion matrix------------------------------------------------------------------------------------------------------------------------------------------

confusion.matrices <- list(
  Majority_Benchmark = tab0,
  LDA = tab1,
  QDA = tab2,
  Naive_Bayes = tab3,
  Logistic_Regression = tab4,
  KNN = tab5
)

print(confusion.matrices)
## $Majority_Benchmark
##          Actual
## Predicted    0    1
##         0 6458 4401
##         1    0    0
## 
## $LDA
##          Actual
## Predicted    0    1
##         0 4569 2268
##         1 1889 2133
## 
## $QDA
##          Actual
## Predicted    0    1
##         0 3668 2216
##         1 2790 2185
## 
## $Naive_Bayes
##          Actual
## Predicted    0    1
##         0 2962 1212
##         1 3496 3189
## 
## $Logistic_Regression
##          Actual
## Predicted    0    1
##         0 4535 2223
##         1 1923 2178
## 
## $KNN
##          Actual
## Predicted    0    1
##         0 3889 2072
##         1 2569 2329