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
## 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
## [1] 1503 6
## '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 ...
## 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
## 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
##
## 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
##
## 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_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_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$ScaledSoundPressureRidge Regression
set.seed(2026)
fit_ridge <- glmnet::cv.glmnet(
x_train, y_train,
alpha = 0,
family = "gaussian",
nfolds = 10
)
plot(fit_ridge)## [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)## 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
## [1] 0.02261894
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.
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
# 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
## Terminal nodes before pruning: 27
## Terminal nodes after pruning: 4
## 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
## %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
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"
)| 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
Recommended model
Based on the held-out test results, Cubist should be selected for this dataset. It produced the lowest RMSE (2.225 dB), meaning that its predictions typically differed from the observed sound-pressure levels by approximately 2.225 dB when larger errors were given extra weight. Its MSE was 4.953, its MAE was 1.518 dB, and its test-set R-squared was 0.901. Therefore, the model explains approximately 90.1% of the variation in sound pressure in the test data.
RMSE was used as the primary selection criterion because all models were evaluated on the same unseen test observations and RMSE penalizes large prediction errors. MAE was reviewed as an easily interpreted average error, MSE confirmed the squared-error ranking, and R-squared measured explained variation. The model-comparison chart supports the decision visually: the selected model has the shortest RMSE bar. Its rule-based linear models provide more interpretation than many black-box methods because the conditions and regression equations can be inspected. Together, the test metrics, visualization, and interpretability assessment provide the basis for the final selection.