Supervised Learning (Regression)

Airfoil Self-Noise

Data Preparation

The Airfoil Self-Noise dataset contains 1,503 observations from NASA aerodynamic and acoustic wind-tunnel experiments. Five decision variables are used to predict the scaled sound-pressure level in decibels.

dataset_path <- file.choose()

airfoil <- read.csv(
  dataset_path,
  header = TRUE,
  check.names = FALSE
)

required_columns <- c(
  "Frequency",
  "AttackAngle",
  "ChordLength",
  "FreeStreamVelocity",
  "SuctionSideDisplacementThickness",
  "ScaledSoundPressure"
)

missing_columns <- setdiff(required_columns, names(airfoil))
extra_columns <- setdiff(names(airfoil), required_columns)

if (length(missing_columns) > 0) {
  stop(
    "The selected dataset is missing these required columns: ",
    paste(missing_columns, collapse = ", "),
    call. = FALSE
  )
}

if (length(extra_columns) > 0) {
  message(
    "Additional columns will not be used in this analysis: ",
    paste(extra_columns, collapse = ", ")
  )
}

airfoil <- airfoil[, required_columns]

if (!all(vapply(airfoil, is.numeric, logical(1)))) {
  stop("All six required columns must contain numeric data.", call. = FALSE)
}

if (anyNA(airfoil)) {
  stop("The selected dataset contains missing values.", call. = FALSE)
}

cat("Selected dataset:", basename(dataset_path), "\n")
## Selected dataset: airfoil_self_noise.csv
head(airfoil)
##   Frequency AttackAngle ChordLength FreeStreamVelocity
## 1       800           0      0.3048               71.3
## 2      1000           0      0.3048               71.3
## 3      1250           0      0.3048               71.3
## 4      1600           0      0.3048               71.3
## 5      2000           0      0.3048               71.3
## 6      2500           0      0.3048               71.3
##   SuctionSideDisplacementThickness ScaledSoundPressure
## 1                       0.00266337             126.201
## 2                       0.00266337             125.201
## 3                       0.00266337             125.951
## 4                       0.00266337             127.591
## 5                       0.00266337             127.461
## 6                       0.00266337             125.571
dim(airfoil)
## [1] 1503    6
str(airfoil)
## 'data.frame':    1503 obs. of  6 variables:
##  $ Frequency                       : int  800 1000 1250 1600 2000 2500 3150 4000 5000 6300 ...
##  $ AttackAngle                     : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ ChordLength                     : num  0.305 0.305 0.305 0.305 0.305 ...
##  $ FreeStreamVelocity              : num  71.3 71.3 71.3 71.3 71.3 71.3 71.3 71.3 71.3 71.3 ...
##  $ SuctionSideDisplacementThickness: num  0.00266 0.00266 0.00266 0.00266 0.00266 ...
##  $ ScaledSoundPressure             : num  126 125 126 128 127 ...
summary(airfoil)
##    Frequency      AttackAngle      ChordLength     FreeStreamVelocity
##  Min.   :  200   Min.   : 0.000   Min.   :0.0254   Min.   :31.70     
##  1st Qu.:  800   1st Qu.: 2.000   1st Qu.:0.0508   1st Qu.:39.60     
##  Median : 1600   Median : 5.400   Median :0.1016   Median :39.60     
##  Mean   : 2886   Mean   : 6.782   Mean   :0.1365   Mean   :50.86     
##  3rd Qu.: 4000   3rd Qu.: 9.900   3rd Qu.:0.2286   3rd Qu.:71.30     
##  Max.   :20000   Max.   :22.200   Max.   :0.3048   Max.   :71.30     
##  SuctionSideDisplacementThickness ScaledSoundPressure
##  Min.   :0.0004007                Min.   :103.4      
##  1st Qu.:0.0025351                1st Qu.:120.2      
##  Median :0.0049574                Median :125.7      
##  Mean   :0.0111399                Mean   :124.8      
##  3rd Qu.:0.0155759                3rd Qu.:130.0      
##  Max.   :0.0584113                Max.   :141.0
colSums(is.na(airfoil))
##                        Frequency                      AttackAngle 
##                                0                                0 
##                      ChordLength               FreeStreamVelocity 
##                                0                                0 
## SuctionSideDisplacementThickness              ScaledSoundPressure 
##                                0                                0

Training and Test Sets

set.seed(2026)
training_rows <- sample(seq_len(nrow(airfoil)), size = floor(0.80 * nrow(airfoil)))

train_data <- airfoil[training_rows, ]
test_data  <- airfoil[-training_rows, ]

predictor_names <- setdiff(names(airfoil), "ScaledSoundPressure")

rmse <- function(actual, predicted) {
  sqrt(mean((actual - predicted)^2))
}

mse <- function(actual, predicted) {
  mean((actual - predicted)^2)
}

mae <- function(actual, predicted) {
  mean(abs(actual - predicted))
}

r_squared <- function(actual, predicted) {
  1 - sum((actual - predicted)^2) /
    sum((actual - mean(actual))^2)
}

model_results <- data.frame(
  Model = character(),
  MSE = numeric(),
  RMSE = numeric(),
  MAE = numeric(),
  R2 = numeric(),
  stringsAsFactors = FALSE
)

add_result <- function(model_name, actual, predicted) {
  data.frame(
    Model = model_name,
    MSE = mse(actual, predicted),
    RMSE = rmse(actual, predicted),
    MAE = mae(actual, predicted),
    R2 = r_squared(actual, predicted)
  )
}

c(Training = nrow(train_data), Test = nrow(test_data))
## Training     Test 
##     1202      301

Ordinary Least Squares Regression

fit_lm <- lm(ScaledSoundPressure ~ ., data = train_data)
summary(fit_lm)
## 
## Call:
## lm(formula = ScaledSoundPressure ~ ., data = train_data)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -17.2105  -2.9460  -0.2066   3.1501  15.6182 
## 
## Coefficients:
##                                    Estimate Std. Error t value Pr(>|t|)    
## (Intercept)                       1.327e+02  6.115e-01 217.078   <2e-16 ***
## Frequency                        -1.283e-03  4.778e-05 -26.858   <2e-16 ***
## AttackAngle                      -3.836e-01  4.459e-02  -8.604   <2e-16 ***
## ChordLength                      -3.514e+01  1.863e+00 -18.863   <2e-16 ***
## FreeStreamVelocity                9.885e-02  9.240e-03  10.698   <2e-16 ***
## SuctionSideDisplacementThickness -1.588e+02  1.720e+01  -9.231   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 4.845 on 1196 degrees of freedom
## Multiple R-squared:  0.5026, Adjusted R-squared:  0.5005 
## F-statistic: 241.7 on 5 and 1196 DF,  p-value: < 2.2e-16
pred_lm <- predict(fit_lm, newdata = test_data)
model_results <- rbind(
  model_results,
  add_result("Ordinary Least Squares", test_data$ScaledSoundPressure, pred_lm)
)
model_results[nrow(model_results), ]
##                    Model      MSE    RMSE      MAE        R2
## 1 Ordinary Least Squares 21.81814 4.67099 3.588869 0.5625693

Stepwise Linear Regression

fit_step <- step(fit_lm, direction = "both", trace = 0)
summary(fit_step)
## 
## Call:
## lm(formula = ScaledSoundPressure ~ Frequency + AttackAngle + 
##     ChordLength + FreeStreamVelocity + SuctionSideDisplacementThickness, 
##     data = train_data)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -17.2105  -2.9460  -0.2066   3.1501  15.6182 
## 
## Coefficients:
##                                    Estimate Std. Error t value Pr(>|t|)    
## (Intercept)                       1.327e+02  6.115e-01 217.078   <2e-16 ***
## Frequency                        -1.283e-03  4.778e-05 -26.858   <2e-16 ***
## AttackAngle                      -3.836e-01  4.459e-02  -8.604   <2e-16 ***
## ChordLength                      -3.514e+01  1.863e+00 -18.863   <2e-16 ***
## FreeStreamVelocity                9.885e-02  9.240e-03  10.698   <2e-16 ***
## SuctionSideDisplacementThickness -1.588e+02  1.720e+01  -9.231   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 4.845 on 1196 degrees of freedom
## Multiple R-squared:  0.5026, Adjusted R-squared:  0.5005 
## F-statistic: 241.7 on 5 and 1196 DF,  p-value: < 2.2e-16
pred_step <- predict(fit_step, newdata = test_data)
model_results <- rbind(
  model_results,
  add_result("Stepwise Linear Regression", test_data$ScaledSoundPressure, pred_step)
)
model_results[nrow(model_results), ]
##                        Model      MSE    RMSE      MAE        R2
## 2 Stepwise Linear Regression 21.81814 4.67099 3.588869 0.5625693

Principal Component Regression

fit_pcr <- pls::pcr(
  ScaledSoundPressure ~ .,
  data = train_data,
  scale = TRUE,
  validation = "CV"
)
summary(fit_pcr)
## Data:    X dimension: 1202 5 
##  Y dimension: 1202 1
## Fit method: svdpc
## Number of components considered: 5
## 
## VALIDATION: RMSEP
## Cross-validated using 10 random segments.
##        (Intercept)  1 comps  2 comps  3 comps  4 comps  5 comps
## CV           6.859    6.854    6.824    6.835    4.934    4.860
## adjCV        6.859    6.854    6.824    6.835    4.933    4.859
## 
## TRAINING: % variance explained
##                      1 comps  2 comps  3 comps  4 comps  5 comps
## X                    42.1813   65.370   83.503    96.60   100.00
## ScaledSoundPressure   0.2279    1.239    1.239    48.68    50.26
pcr_rmsep <- pls::RMSEP(fit_pcr)
plot(pcr_rmsep, main = "PCR Cross-Validated RMSEP")

pcr_components <- max(1, pls::selectNcomp(fit_pcr, method = "onesigma", plot = FALSE))
pred_pcr <- as.numeric(predict(fit_pcr, newdata = test_data, ncomp = pcr_components))
model_results <- rbind(
  model_results,
  add_result("Principal Component Regression", test_data$ScaledSoundPressure, pred_pcr)
)
model_results[nrow(model_results), ]
##                            Model      MSE     RMSE      MAE        R2
## 3 Principal Component Regression 23.69457 4.867707 3.755205 0.5249489

Partial Least Squares Regression

fit_pls <- pls::plsr(
  ScaledSoundPressure ~ .,
  data = train_data,
  scale = TRUE,
  validation = "CV"
)
summary(fit_pls)
## Data:    X dimension: 1202 5 
##  Y dimension: 1202 1
## Fit method: kernelpls
## Number of components considered: 5
## 
## VALIDATION: RMSEP
## Cross-validated using 10 random segments.
##        (Intercept)  1 comps  2 comps  3 comps  4 comps  5 comps
## CV           6.859    4.985    4.898    4.881    4.861    4.862
## adjCV        6.859    4.978    4.898    4.880    4.859    4.860
## 
## TRAINING: % variance explained
##                      1 comps  2 comps  3 comps  4 comps  5 comps
## X                      14.98    52.74    75.94    81.87   100.00
## ScaledSoundPressure    47.81    49.44    49.79    50.26    50.26
pls_rmsep <- pls::RMSEP(fit_pls)
plot(pls_rmsep, main = "PLS Cross-Validated RMSEP")

pls_components <- max(1, pls::selectNcomp(fit_pls, method = "onesigma", plot = FALSE))
pred_pls <- as.numeric(predict(fit_pls, newdata = test_data, ncomp = pls_components))
model_results <- rbind(
  model_results,
  add_result("Partial Least Squares", test_data$ScaledSoundPressure, pred_pls)
)
model_results[nrow(model_results), ]
##                   Model      MSE     RMSE      MAE        R2
## 4 Partial Least Squares 23.44062 4.841552 3.836379 0.5300403

Penalized Regression

x_train <- model.matrix(ScaledSoundPressure ~ ., train_data)[, -1]
y_train <- train_data$ScaledSoundPressure
x_test  <- model.matrix(ScaledSoundPressure ~ ., test_data)[, -1]
y_test  <- test_data$ScaledSoundPressure

Ridge Regression

set.seed(2026)
fit_ridge <- glmnet::cv.glmnet(
  x_train, y_train,
  alpha = 0,
  family = "gaussian",
  nfolds = 10
)
plot(fit_ridge)

fit_ridge$lambda.min
## [1] 0.2661824
pred_ridge <- as.numeric(predict(fit_ridge, newx = x_test, s = "lambda.min"))
model_results <- rbind(
  model_results,
  add_result("Ridge Regression", y_test, pred_ridge)
)
model_results[nrow(model_results), ]
##              Model      MSE     RMSE      MAE       R2
## 5 Ridge Regression 22.05468 4.696241 3.645806 0.557827

Least Absolute Shrinkage and Selection Operator

set.seed(2026)
fit_lasso <- glmnet::cv.glmnet(
  x_train, y_train,
  alpha = 1,
  family = "gaussian",
  nfolds = 10
)
plot(fit_lasso)

coef(fit_lasso, s = "lambda.min")
## 6 x 1 sparse Matrix of class "dgCMatrix"
##                                     lambda.min
## (Intercept)                       1.326897e+02
## Frequency                        -1.276441e-03
## AttackAngle                      -3.770948e-01
## ChordLength                      -3.484332e+01
## FreeStreamVelocity                9.780913e-02
## SuctionSideDisplacementThickness -1.594035e+02
pred_lasso <- as.numeric(predict(fit_lasso, newx = x_test, s = "lambda.min"))
model_results <- rbind(
  model_results,
  add_result("LASSO", y_test, pred_lasso)
)
model_results[nrow(model_results), ]
##   Model      MSE     RMSE      MAE        R2
## 6 LASSO 21.83697 4.673005 3.593184 0.5621918

Elastic Net

set.seed(2026)
elastic_candidates <- lapply(seq(0.1, 0.9, by = 0.1), function(a) {
  model <- glmnet::cv.glmnet(
    x_train, y_train,
    alpha = a,
    family = "gaussian",
    nfolds = 10
  )
  list(alpha = a, model = model, error = min(model$cvm))
})

best_elastic <- elastic_candidates[[
  which.min(vapply(elastic_candidates, `[[`, numeric(1), "error"))
]]

best_elastic$alpha
## [1] 0.1
best_elastic$model$lambda.min
## [1] 0.02261894
plot(best_elastic$model)

pred_elastic <- as.numeric(
  predict(best_elastic$model, newx = x_test, s = "lambda.min")
)
model_results <- rbind(
  model_results,
  add_result("Elastic Net", y_test, pred_elastic)
)
model_results[nrow(model_results), ]
##         Model      MSE     RMSE      MAE        R2
## 7 Elastic Net 21.83744 4.673055 3.593762 0.5621824

Non-Linear Regression

Support Vector Regression

set.seed(2026)
fit_svr <- e1071::tune.svm(
  ScaledSoundPressure ~ .,
  data = train_data,
  kernel = "radial",
  gamma = 2^(-6:-3),
  cost = 2^(1:4),
  tunecontrol = e1071::tune.control(cross = 5)
)
fit_svr$best.parameters
##    gamma cost
## 16 0.125   16
pred_svr <- predict(fit_svr$best.model, newdata = test_data)
model_results <- rbind(
  model_results,
  add_result("Support Vector Regression", y_test, pred_svr)
)
model_results[nrow(model_results), ]
##                       Model      MSE     RMSE      MAE        R2
## 8 Support Vector Regression 6.860452 2.619247 1.962319 0.8624552

KNN

set.seed(2026)
fit_knn <- caret::train(
  ScaledSoundPressure ~ .,
  data = train_data,
  method = "knn",
  preProcess = c("center", "scale"),
  tuneGrid = data.frame(k = seq(3, 25, by = 2)),
  trControl = caret::trainControl(method = "cv", number = 10)
)
fit_knn
## k-Nearest Neighbors 
## 
## 1202 samples
##    5 predictor
## 
## Pre-processing: centered (5), scaled (5) 
## Resampling: Cross-Validated (10 fold) 
## Summary of sample sizes: 1082, 1082, 1081, 1082, 1082, 1082, ... 
## Resampling results across tuning parameters:
## 
##   k   RMSE      Rsquared   MAE     
##    3  2.860391  0.8332373  2.140857
##    5  3.330323  0.7738720  2.519960
##    7  3.599338  0.7349125  2.772781
##    9  3.712032  0.7194420  2.864979
##   11  3.736544  0.7211360  2.891942
##   13  3.831003  0.7088916  2.983087
##   15  3.931724  0.6933784  3.083285
##   17  4.002584  0.6829580  3.155335
##   19  4.029147  0.6803342  3.198989
##   21  4.045148  0.6818416  3.209045
##   23  4.057362  0.6837647  3.204060
##   25  4.098436  0.6790540  3.230952
## 
## RMSE was used to select the optimal model using the smallest value.
## The final value used for the model was k = 3.
plot(fit_knn)

pred_knn <- predict(fit_knn, newdata = test_data)
model_results <- rbind(
  model_results,
  add_result("KNN", y_test, pred_knn)
)
model_results[nrow(model_results), ]
##   Model      MSE     RMSE      MAE        R2
## 9   KNN 5.561085 2.358195 1.791998 0.8885061

Non-Linear Regression with Decision Trees

set.seed(2026)

# First grow a reasonably large tree. A small cp allows candidate branches to
# form; cross-validation below decides which branches should be removed.
fit_tree_full <- rpart::rpart(
  ScaledSoundPressure ~ .,
  data = train_data,
  method = "anova",
  control = rpart::rpart.control(
    cp = 0.001,
    minsplit = 60,
    minbucket = 25,
    maxdepth = 6,
    xval = 10
  )
)

# Examine cross-validated error for the candidate tree sizes.
rpart::printcp(fit_tree_full)
## 
## Regression tree:
## rpart::rpart(formula = ScaledSoundPressure ~ ., data = train_data, 
##     method = "anova", control = rpart::rpart.control(cp = 0.001, 
##         minsplit = 60, minbucket = 25, maxdepth = 6, xval = 10))
## 
## Variables actually used in tree construction:
## [1] AttackAngle                      ChordLength                     
## [3] FreeStreamVelocity               Frequency                       
## [5] SuctionSideDisplacementThickness
## 
## Root node error: 56450/1202 = 46.964
## 
## n= 1202 
## 
##           CP nsplit rel error  xerror     xstd
## 1  0.1673837      0   1.00000 1.00101 0.037471
## 2  0.0592524      2   0.66523 0.68494 0.028874
## 3  0.0290652      3   0.60598 0.64207 0.026772
## 4  0.0250529      4   0.57692 0.61887 0.024640
## 5  0.0239305      6   0.52681 0.58123 0.023426
## 6  0.0236108      7   0.50288 0.57946 0.023408
## 7  0.0192027      8   0.47927 0.54914 0.023205
## 8  0.0174044      9   0.46007 0.51979 0.022068
## 9  0.0160684     10   0.44266 0.50459 0.021520
## 10 0.0123718     11   0.42659 0.50631 0.021756
## 11 0.0103836     13   0.40185 0.48218 0.021004
## 12 0.0101124     14   0.39147 0.48436 0.021138
## 13 0.0098797     16   0.37124 0.48473 0.021065
## 14 0.0073118     19   0.34160 0.46841 0.020838
## 15 0.0063486     21   0.32698 0.44992 0.019829
## 16 0.0056459     22   0.32063 0.44448 0.019460
## 17 0.0033858     23   0.31498 0.42694 0.018746
## 18 0.0028942     24   0.31160 0.42819 0.018681
## 19 0.0025393     25   0.30870 0.42583 0.018467
## 20 0.0010000     26   0.30616 0.42243 0.018306
rpart::plotcp(fit_tree_full)

# Select a CP that limits the explanatory tree to at most three splits. This
# guarantees no more than four terminal nodes, matching the reference layout.
cp_table <- fit_tree_full$cptable
eligible_rows <- which(cp_table[, "nsplit"] <= 3)
selected_row <- eligible_rows[
  which.max(cp_table[eligible_rows, "nsplit"])
]
selected_cp <- cp_table[selected_row, "CP"]

# Prune away branches that do not provide enough cross-validated improvement.
fit_tree <- rpart::prune(fit_tree_full, cp = selected_cp)

cat("Selected pruning CP:", selected_cp, "\n")
## Selected pruning CP: 0.0290652
cat("Terminal nodes before pruning:", sum(fit_tree_full$frame$var == "<leaf>"), "\n")
## Terminal nodes before pruning: 27
cat("Terminal nodes after pruning:", sum(fit_tree$frame$var == "<leaf>"), "\n")
## Terminal nodes after pruning: 4
print(fit_tree)
## n= 1202 
## 
## node), split, n, deviance, yval
##       * denotes terminal node
## 
## 1) root 1202 56450.180 124.8921  
##   2) Frequency>=3575 336 19989.230 120.4604  
##     4) SuctionSideDisplacementThickness>=0.001562845 247  7461.787 117.2288 *
##     5) SuctionSideDisplacementThickness< 0.001562845 89  2789.272 129.4289 *
##   3) Frequency< 3575 866 27301.450 126.6116  
##     6) SuctionSideDisplacementThickness>=0.0155759 246 10416.530 123.4916 *
##     7) SuctionSideDisplacementThickness< 0.0155759 620 13540.100 127.8495 *
rpart.plot::rpart.plot(
  fit_tree,
  main = "Classification and Regression Trees",
  type = 2,
  extra = 101,
  fallen.leaves = TRUE,
  box.palette = "Blues",
  shadow.col = "gray",
  branch = 0.5,
  tweak = 1.15,
  split.cex = 1.05,
  nn = FALSE,
  roundint = FALSE
)

rattle::fancyRpartPlot(
  fit_tree,
  main = "Airfoil Self-Noise Regression Tree",
  type = 2,
  palettes = c("Greens"),
  tweak = 1.15,
  sub = ""
)

pred_tree <- predict(fit_tree, newdata = test_data)
model_results <- rbind(
  model_results,
  add_result("Regression Tree", y_test, pred_tree)
)
model_results[nrow(model_results), ]
##              Model      MSE     RMSE      MAE        R2
## 10 Regression Tree 30.21996 5.497268 4.466197 0.3941218

Conditional Inference Tree

fit_ctree <- partykit::ctree(
  ScaledSoundPressure ~ .,
  data = train_data,
  control = partykit::ctree_control(
    mincriterion = 0.99,
    minsplit = 100,
    minbucket = 50,
    maxdepth = 2
  )
)
plot(
  fit_ctree,
  type = "simple",
  gp = grid::gpar(fontsize = 12)
)

pred_ctree <- predict(fit_ctree, newdata = test_data)
model_results <- rbind(
  model_results,
  add_result("Conditional Inference Tree", y_test, pred_ctree)
)
model_results[nrow(model_results), ]
##                         Model      MSE     RMSE      MAE        R2
## 11 Conditional Inference Tree 33.62576 5.798772 4.669993 0.3258391

Bagging CART

set.seed(2026)
fit_bagging <- ipred::bagging(
  ScaledSoundPressure ~ .,
  data = train_data,
  nbagg = 100,
  coob = TRUE,
  control = rpart::rpart.control(minsplit = 20, cp = 0)
)
fit_bagging
## 
## Bagging regression trees with 100 bootstrap replications 
## 
## Call: bagging.data.frame(formula = ScaledSoundPressure ~ ., data = train_data, 
##     nbagg = 100, coob = TRUE, control = rpart::rpart.control(minsplit = 20, 
##         cp = 0))
## 
## Out-of-bag estimate of root mean squared error:  2.5875
pred_bagging <- predict(fit_bagging, newdata = test_data)
model_results <- rbind(
  model_results,
  add_result("Bagging CART", y_test, pred_bagging)
)
model_results[nrow(model_results), ]
##           Model      MSE     RMSE      MAE        R2
## 12 Bagging CART 5.619788 2.370609 1.802847 0.8873292

Random Forest

set.seed(2026)
fit_rf <- randomForest::randomForest(
  ScaledSoundPressure ~ .,
  data = train_data,
  ntree = 500,
  importance = TRUE
)
fit_rf
## 
## Call:
##  randomForest(formula = ScaledSoundPressure ~ ., data = train_data,      ntree = 500, importance = TRUE) 
##                Type of random forest: regression
##                      Number of trees: 500
## No. of variables tried at each split: 1
## 
##           Mean of squared residuals: 13.30314
##                     % Var explained: 71.67
randomForest::importance(fit_rf)
##                                   %IncMSE IncNodePurity
## Frequency                        66.70730     15621.926
## AttackAngle                      40.25373      5226.341
## ChordLength                      47.64225      5469.542
## FreeStreamVelocity               32.51756      1843.974
## SuctionSideDisplacementThickness 42.03728      8312.329
randomForest::varImpPlot(fit_rf)

pred_rf <- predict(fit_rf, newdata = test_data)
model_results <- rbind(
  model_results,
  add_result("Random Forest", y_test, pred_rf)
)
model_results[nrow(model_results), ]
##            Model     MSE     RMSE     MAE        R2
## 13 Random Forest 12.4286 3.525422 2.89283 0.7508197

Cubist

set.seed(2026)
fit_cubist <- Cubist::cubist(
  x = train_data[, predictor_names],
  y = train_data$ScaledSoundPressure,
  committees = 1,
  neighbors = 0
)
summary(fit_cubist)
## 
## Call:
## cubist.default(x = train_data[, predictor_names], y
##  = train_data$ScaledSoundPressure, committees = 1, neighbors = 0)
## 
## 
## Cubist [Release 2.07 GPL Edition]
## ---------------------------------
## 
##     Target attribute `outcome'
## 
## Read 1202 cases (6 attributes) from undefined.data
## 
## Model:
## 
##   Rule 1: [148 cases, mean 116.7811, range 103.38 to 128.707, est err 1.2649]
## 
##     if
##  Frequency > 1600
##  ChordLength > 0.1016
##  SuctionSideDisplacementThickness > 0.00392107
##     then
##  outcome = 131.7325 - 1.08 AttackAngle - 0.00161 Frequency
##            - 29.8 ChordLength + 0.121 FreeStreamVelocity
##            - 99 SuctionSideDisplacementThickness
## 
##   Rule 2: [19 cases, mean 119.1030, range 108.649 to 127.688, est err 2.2276]
## 
##     if
##  Frequency > 8000
##  AttackAngle > 2
##  SuctionSideDisplacementThickness <= 0.00392107
##     then
##  outcome = 103.6749 + 5.85 AttackAngle
##            - 183 SuctionSideDisplacementThickness - 0.00058 Frequency
##            + 0.007 FreeStreamVelocity
## 
##   Rule 3: [35 cases, mean 121.2071, range 110.491 to 137.026, est err 3.4686]
## 
##     if
##  Frequency <= 630
##  AttackAngle > 15.6
##     then
##  outcome = 109.2248 + 0.02651 Frequency
##            + 386 SuctionSideDisplacementThickness
##            - 0.164 FreeStreamVelocity
## 
##   Rule 4: [151 cases, mean 121.5167, range 106.604 to 136.166, est err 1.9855]
## 
##     if
##  Frequency > 1600
##  ChordLength <= 0.1016
##  SuctionSideDisplacementThickness > 0.00392107
##     then
##  outcome = 138.0251 - 131.8 ChordLength - 0.0021 Frequency
##            - 0.56 AttackAngle + 0.169 FreeStreamVelocity
##            - 128 SuctionSideDisplacementThickness
## 
##   Rule 5: [112 cases, mean 121.9006, range 106.111 to 133.04, est err 1.0197]
## 
##     if
##  Frequency > 1600
##  AttackAngle <= 2
##  ChordLength > 0.0508
##  FreeStreamVelocity > 31.7
##     then
##  outcome = 134.3537 - 1635 SuctionSideDisplacementThickness
##            - 0.00125 Frequency - 26.8 ChordLength - 0.4 AttackAngle
##            + 0.085 FreeStreamVelocity
## 
##   Rule 6: [32 cases, mean 123.5455, range 108.265 to 135.49, est err 1.1544]
## 
##     if
##  Frequency > 1600
##  AttackAngle <= 2
##  ChordLength > 0.0254
##  FreeStreamVelocity <= 31.7
##     then
##  outcome = 147.3118 - 11348 SuctionSideDisplacementThickness
##            + 5.18 AttackAngle - 0.0018 Frequency + 52.3 ChordLength
## 
##   Rule 7: [31 cases, mean 125.8087, range 116.659 to 133.894, est err 1.1890]
## 
##     if
##  Frequency > 1600
##  Frequency <= 8000
##  AttackAngle > 2
##  SuctionSideDisplacementThickness > 0.00172668
##  SuctionSideDisplacementThickness <= 0.00392107
##     then
##  outcome = 113.3192 + 5.56 AttackAngle - 0.00216 Frequency
##            + 0.096 FreeStreamVelocity
## 
##   Rule 8: [20 cases, mean 125.8225, range 119.975 to 133.13, est err 1.0911]
## 
##     if
##  Frequency <= 630
##  AttackAngle <= 15.6
##  ChordLength <= 0.0508
##  FreeStreamVelocity <= 39.6
##  SuctionSideDisplacementThickness > 0.0104404
##     then
##  outcome = 121.3212 + 0.01425 Frequency + 100.9 ChordLength
##            - 1.11 AttackAngle + 0.256 FreeStreamVelocity
## 
##   Rule 9: [126 cases, mean 126.2746, range 112.16 to 140.158, est err 2.1013]
## 
##     if
##  Frequency <= 1600
##  ChordLength > 0.0508
##  SuctionSideDisplacementThickness > 0.0104404
##     then
##  outcome = 134.6672 - 0.00751 Frequency
##            - 382 SuctionSideDisplacementThickness - 32.4 ChordLength
##            + 0.174 FreeStreamVelocity + 0.44 AttackAngle
## 
##   Rule 10: [57 cases, mean 126.4319, range 116.074 to 136.758, est err 1.3826]
## 
##     if
##  Frequency <= 1000
##  ChordLength <= 0.1016
##  SuctionSideDisplacementThickness <= 0.0052139
##     then
##  outcome = 107.6128 + 0.01509 Frequency
##            + 1674 SuctionSideDisplacementThickness + 68 ChordLength
##            - 0.017 FreeStreamVelocity - 0.01 AttackAngle
## 
##   Rule 11: [111 cases, mean 127.1629, range 117.195 to 139.918, est err 1.6165]
## 
##     if
##  Frequency <= 630
##  ChordLength > 0.1016
##  SuctionSideDisplacementThickness <= 0.0104404
##     then
##  outcome = 109.0855 + 0.01713 Frequency
##            + 1707 SuctionSideDisplacementThickness
##            + 0.041 FreeStreamVelocity
## 
##   Rule 12: [48 cases, mean 127.2307, range 114.477 to 140.987, est err 2.7665]
## 
##     if
##  Frequency > 1000
##  Frequency <= 1600
##  ChordLength <= 0.1016
##  SuctionSideDisplacementThickness > 0.0052139
##     then
##  outcome = 133.9078 - 37.8 ChordLength - 0.00106 Frequency
##            - 0.32 AttackAngle - 132 SuctionSideDisplacementThickness
##            + 0.093 FreeStreamVelocity
## 
##   Rule 13: [19 cases, mean 127.7489, range 122.94 to 131.865, est err 1.8298]
## 
##     if
##  Frequency <= 1600
##  FreeStreamVelocity > 39.6
##  SuctionSideDisplacementThickness > 0.0233328
##  SuctionSideDisplacementThickness <= 0.0289853
##     then
##  outcome = 124.9863 + 0.00425 Frequency
##            - 9 SuctionSideDisplacementThickness
##            + 0.005 FreeStreamVelocity
## 
##   Rule 14: [25 cases, mean 128.0452, range 118.634 to 140.987, est err 4.9552]
## 
##     if
##  Frequency > 630
##  Frequency <= 1600
##  AttackAngle > 15.6
##     then
##  outcome = 137.8516 - 498 SuctionSideDisplacementThickness
## 
##   Rule 15: [32 cases, mean 128.4940, range 120.154 to 140.158, est err 2.9252]
## 
##     if
##  Frequency <= 1600
##  AttackAngle <= 15.6
##  FreeStreamVelocity > 39.6
##  SuctionSideDisplacementThickness > 0.0289853
##     then
##  outcome = 149.9645 - 0.00819 Frequency
##            - 410 SuctionSideDisplacementThickness
##            + 0.007 FreeStreamVelocity
## 
##   Rule 16: [119 cases, mean 128.6727, range 120.058 to 136.023, est err 1.5978]
## 
##     if
##  Frequency > 630
##  Frequency <= 1600
##  ChordLength > 0.1016
##  SuctionSideDisplacementThickness <= 0.0104404
##     then
##  outcome = 137.1471 - 0.00469 Frequency
##            - 801 SuctionSideDisplacementThickness + 0.58 AttackAngle
##            - 15.3 ChordLength + 0.051 FreeStreamVelocity
## 
##   Rule 17: [24 cases, mean 128.9445, range 120.015 to 138.523, est err 2.0483]
## 
##     if
##  Frequency <= 1000
##  ChordLength <= 0.1016
##  SuctionSideDisplacementThickness > 0.0052139
##  SuctionSideDisplacementThickness <= 0.0104404
##     then
##  outcome = 107.792 + 0.00986 Frequency
##            + 1352 SuctionSideDisplacementThickness + 96.1 ChordLength
##            - 0.02 AttackAngle
## 
##   Rule 18: [38 cases, mean 130.1316, range 121.635 to 136.941, est err 3.9453]
## 
##     if
##  Frequency <= 1600
##  AttackAngle <= 15.6
##  FreeStreamVelocity > 39.6
##  SuctionSideDisplacementThickness > 0.0104404
##  SuctionSideDisplacementThickness <= 0.0233328
##     then
##  outcome = 128.3089 - 0.00361 Frequency
##            - 175 SuctionSideDisplacementThickness
##            + 0.099 FreeStreamVelocity
## 
##   Rule 19: [29 cases, mean 130.3011, range 123.255 to 136.941, est err 4.4883]
## 
##     if
##  Frequency > 630
##  Frequency <= 1600
##  AttackAngle <= 15.6
##  ChordLength <= 0.0508
##  SuctionSideDisplacementThickness > 0.0104404
##     then
##  outcome = 132.5353 - 0.00449 Frequency + 0.339 FreeStreamVelocity
##            - 0.71 AttackAngle - 90 SuctionSideDisplacementThickness
##            - 9.2 ChordLength
## 
##   Rule 20: [16 cases, mean 130.8829, range 120.397 to 135.484, est err 1.9842]
## 
##     if
##  Frequency > 4000
##  Frequency <= 8000
##  AttackAngle > 2
##  SuctionSideDisplacementThickness <= 0.00172668
##     then
##  outcome = 102.5968 + 8.77 AttackAngle - 0.00188 Frequency
##            + 0.014 FreeStreamVelocity - 2.1 ChordLength
##            - 10 SuctionSideDisplacementThickness
## 
##   Rule 21: [20 cases, mean 130.9003, range 123.988 to 135.938, est err 1.8602]
## 
##     if
##  Frequency > 1600
##  AttackAngle <= 2
##  ChordLength > 0.0254
##  ChordLength <= 0.0508
##  FreeStreamVelocity > 31.7
##     then
##  outcome = 136.956 - 0.00085 Frequency
## 
##   Rule 22: [40 cases, mean 131.0211, range 121.474 to 137.658, est err 1.2723]
## 
##     if
##  Frequency > 1000
##  Frequency <= 1600
##  ChordLength <= 0.1016
##  SuctionSideDisplacementThickness <= 0.0052139
##     then
##  outcome = 124.6764 + 1867 SuctionSideDisplacementThickness
##            + 46.2 ChordLength - 0.34 AttackAngle + 0.00041 Frequency
## 
##   Rule 23: [16 cases, mean 131.3626, range 124.156 to 138.557, est err 2.0894]
## 
##     if
##  Frequency <= 4000
##  AttackAngle <= 2
##  ChordLength <= 0.0254
##     then
##  outcome = 123.686 + 0.00317 Frequency
## 
##   Rule 24: [21 cases, mean 132.1714, range 125.054 to 135.328, est err 1.4405]
## 
##     if
##  Frequency > 1600
##  Frequency <= 4000
##  AttackAngle > 2
##  SuctionSideDisplacementThickness <= 0.00172668
##     then
##  outcome = 125.356 + 0.00241 Frequency
## 
##   Rule 25: [22 cases, mean 132.4084, range 121.933 to 138.607, est err 1.9241]
## 
##     if
##  Frequency > 4000
##  AttackAngle <= 2
##  ChordLength <= 0.0254
##     then
##  outcome = 141.737 - 0.00098 Frequency
## 
## 
## Evaluation on training data (1202 cases):
## 
##     Average  |error|             1.4495
##     Relative |error|               0.26
##     Correlation coefficient        0.95
## 
## 
##  Attribute usage:
##    Conds  Model
## 
##    100%    98%    Frequency
##     82%    70%    ChordLength
##     80%    90%    SuctionSideDisplacementThickness
##     36%    74%    AttackAngle
##     21%    85%    FreeStreamVelocity
pred_cubist <- predict(fit_cubist, newdata = test_data[, predictor_names])
model_results <- rbind(
  model_results,
  add_result("Cubist", y_test, pred_cubist)
)
model_results[nrow(model_results), ]
##     Model     MSE     RMSE      MAE        R2
## 14 Cubist 4.95277 2.225482 1.518106 0.9007022

Model Comparison

model_results <- model_results[order(model_results$RMSE), ]
rownames(model_results) <- NULL

knitr::kable(
  model_results,
  digits = 3,
  caption = "Test-set performance of the regression models"
)
Test-set performance of the regression models
Model MSE RMSE MAE R2
Cubist 4.953 2.225 1.518 0.901
KNN 5.561 2.358 1.792 0.889
Bagging CART 5.620 2.371 1.803 0.887
Support Vector Regression 6.860 2.619 1.962 0.862
Random Forest 12.429 3.525 2.893 0.751
Ordinary Least Squares 21.818 4.671 3.589 0.563
Stepwise Linear Regression 21.818 4.671 3.589 0.563
LASSO 21.837 4.673 3.593 0.562
Elastic Net 21.837 4.673 3.594 0.562
Ridge Regression 22.055 4.696 3.646 0.558
Partial Least Squares 23.441 4.842 3.836 0.530
Principal Component Regression 23.695 4.868 3.755 0.525
Regression Tree 30.220 5.497 4.466 0.394
Conditional Inference Tree 33.626 5.799 4.670 0.326
comparison_colors <- ifelse(
  model_results$RMSE == min(model_results$RMSE),
  "#AD1F46",
  "#75A9C6"
)

par(mar = c(5, 12, 4, 2))
barplot(
  rev(model_results$RMSE),
  names.arg = rev(model_results$Model),
  horiz = TRUE,
  las = 1,
  col = rev(comparison_colors),
  border = NA,
  xlab = "RMSE",
  main = "Regression Model Comparison"
)

Model Selection and Interpretation