Question: What are the predictors and the response?
data("permeability", package = "AppliedPredictiveModeling")
x_perm <- as.data.frame(fingerprints)
names(x_perm) <- paste0("Fingerprint", seq_len(ncol(x_perm)))
y_perm <- as.numeric(permeability)
dim(x_perm)
## [1] 165 1107
length(y_perm)
## [1] 165
head(x_perm[, 1:6])
## Fingerprint1 Fingerprint2 Fingerprint3 Fingerprint4 Fingerprint5 Fingerprint6
## 1 0 0 0 0 0 1
## 2 0 0 0 0 0 0
## 3 0 0 0 0 0 1
## 4 0 0 0 0 0 0
## 5 0 0 0 0 0 0
## 6 0 0 0 0 0 0
Answer: The dataset contains 165 compounds and 1107 binary fingerprint predictors. A 1 indicates that a particular substructure is present; a 0 indicates it is absent. The response is the measured permeability of each compound.
Question: How many predictors remain after filtering out the rare fingerprints?
rare <- nearZeroVar(x_perm)
data.frame(
Before = ncol(x_perm),
Removed = length(rare),
Remaining = ncol(x_perm) - length(rare)
)
## Before Removed Remaining
## 1 1107 719 388
Answer: Using the default settings in
nearZeroVar(), I removed 719 predictors,
leaving 388. This is the count for the full dataset. In
the modeling steps, I have train() repeat the filter using
only the fitting data so that the validation and test observations do
not influence preprocessing.
Question: How many latent variables are optimal, and what is the corresponding cross-validation R²?
First, I split the observations into 80% training and 20% test. I save the training folds so the other models will be compared on the same splits.
set.seed(6242)
train_rows_perm <- sort(sample.int(nrow(x_perm), floor(0.8 * nrow(x_perm))))
x_train_perm <- x_perm[train_rows_perm, ]
x_test_perm <- x_perm[-train_rows_perm, ]
y_train_perm <- y_perm[train_rows_perm]
y_test_perm <- y_perm[-train_rows_perm]
set.seed(16242)
folds_perm <- createFolds(y_train_perm, k = 10, returnTrain = TRUE)
cv_perm <- trainControl(method = "cv", number = 10, index = folds_perm)
c(Training = nrow(x_train_perm), Test = nrow(x_test_perm))
## Training Test
## 132 33
Next, I use caret::train() to fit partial least squares
(PLS). The model tries one through 20 components. Preprocessing removes
sparse fingerprints, centers each retained predictor, and scales it. I
choose the number of components with the lowest cross-validation
RMSE.
prep_perm <- c("nzv", "center", "scale")
set.seed(6242)
pls_fit <- train(
x = x_train_perm, y = y_train_perm,
method = "pls",
preProcess = prep_perm,
tuneGrid = data.frame(ncomp = 1:20),
trControl = cv_perm,
metric = "RMSE"
)
pls_best <- pls_fit$results[
pls_fit$results$ncomp == pls_fit$bestTune$ncomp,
c("ncomp", "RMSE", "Rsquared")
]
knitr::kable(pls_best, digits = 3, row.names = FALSE)
| ncomp | RMSE | Rsquared |
|---|---|---|
| 2 | 10.63 | 0.57 |
plot(pls_fit, main = "Permeability: tuning PLS")
Answer: The best model uses 2 latent variables. Its mean cross-validation R² is 0.57, with an RMSE of 10.63. The R² reported by caret is based on squared correlation between the observed and predicted values.
Question: What is the test-set R²?
pls_pred <- predict(pls_fit, newdata = x_test_perm)
pls_test <- postResample(pred = pls_pred, obs = y_test_perm)
round(pls_test, 3)
## RMSE Rsquared MAE
## 13.907 0.350 9.470
limits <- range(y_test_perm, pls_pred)
plot(y_test_perm, pls_pred, pch = 19, col = "steelblue",
xlim = limits, ylim = limits,
xlab = "Observed permeability", ylab = "Predicted permeability",
main = "PLS predictions on the test set")
abline(0, 1, lty = 2)
Answer: The test-set R² is 0.35, and test RMSE is 13.907. The dashed line shows where the points would fall if the predictions exactly matched the observed values.
Question: Do any other models predict permeability better?
I compare PLS with principal component regression (PCR) and ridge
regression using the same cross-validation folds. PCR chooses components
from the predictors without looking at permeability. Ridge keeps the
predictors but shrinks their coefficients. In glmnet,
alpha = 0 specifies ridge regression; lambda
controls the amount of shrinkage.
set.seed(6242)
pcr_fit <- train(
x = x_train_perm, y = y_train_perm,
method = "pcr",
preProcess = prep_perm,
tuneGrid = data.frame(ncomp = 1:20),
trControl = cv_perm,
metric = "RMSE"
)
pcr_fit$bestTune
## ncomp
## 10 10
ridge_grid <- expand.grid(alpha = 0, lambda = 10^seq(-3, 2, length.out = 26))
set.seed(6242)
ridge_perm <- train(
x = x_train_perm, y = y_train_perm,
method = "glmnet",
preProcess = prep_perm,
tuneGrid = ridge_grid,
trControl = cv_perm,
metric = "RMSE",
standardize = FALSE,
lambda = sort(ridge_grid$lambda, decreasing = TRUE)
)
ridge_perm$bestTune
## alpha lambda
## 21 0 10
pcr_pred <- predict(pcr_fit, newdata = x_test_perm)
ridge_pred <- predict(ridge_perm, newdata = x_test_perm)
pcr_test <- postResample(pred = pcr_pred, obs = y_test_perm)
ridge_test <- postResample(pred = ridge_pred, obs = y_test_perm)
comparison <- data.frame(
Model = c("PLS", "PCR", "Ridge"),
CV_RMSE = c(min(pls_fit$results$RMSE), min(pcr_fit$results$RMSE),
min(ridge_perm$results$RMSE)),
Test_RMSE = c(pls_test[["RMSE"]], pcr_test[["RMSE"]], ridge_test[["RMSE"]]),
Test_R2 = c(pls_test[["Rsquared"]], pcr_test[["Rsquared"]],
ridge_test[["Rsquared"]])
)
knitr::kable(comparison, digits = 3, row.names = FALSE)
| Model | CV_RMSE | Test_RMSE | Test_R2 |
|---|---|---|---|
| PLS | 10.630 | 13.907 | 0.350 |
| PCR | 10.815 | 13.949 | 0.350 |
| Ridge | 10.150 | 13.541 | 0.381 |
Answer: Ridge had the lowest cross-validation RMSE. On this test split, Ridge had the lowest RMSE and Ridge had the highest R². These comparisons are based on one split, so they do not guarantee that the same ranking will hold on new data.
Question: Would I recommend replacing the laboratory experiment with any of these models?
Answer: No. I would use the model to help decide which compounds to test first. The dataset is small, and a model could make a wrong decision about a compound near the permeability cutoff. Before replacing any lab measurements, I would check the model on new compounds and see how often it incorrectly classifies compounds near that cutoff.
Question: What are the predictors and the response?
data("ChemicalManufacturingProcess", package = "AppliedPredictiveModeling")
manufacturing <- ChemicalManufacturingProcess
x_mfg <- manufacturing[, names(manufacturing) != "Yield"]
y_mfg <- manufacturing$Yield
data.frame(
Runs = nrow(x_mfg),
Biological = sum(grepl("^BiologicalMaterial", names(x_mfg))),
Manufacturing = sum(grepl("^ManufacturingProcess", names(x_mfg))),
Total_predictors = ncol(x_mfg)
)
## Runs Biological Manufacturing Total_predictors
## 1 176 12 45 57
Answer: The response is Yield, measured as a percent. The dataset has 176 runs and 57 predictors, including 12 biological measurements and 45 manufacturing process measurements.
Question: How can I fill in the missing predictor values?
I first count the missing values. Then I split the data before
imputing so the test observations do not affect the medians. I use
preProcess() here to show what median imputation does. In
part (c), train() repeats the imputation inside each
cross-validation fold.
set.seed(6243)
train_rows_mfg <- sort(sample.int(nrow(x_mfg), floor(0.8 * nrow(x_mfg))))
x_train_mfg <- x_mfg[train_rows_mfg, ]
x_test_mfg <- x_mfg[-train_rows_mfg, ]
y_train_mfg <- y_mfg[train_rows_mfg]
y_test_mfg <- y_mfg[-train_rows_mfg]
imputer <- preProcess(x_train_mfg, method = "medianImpute")
train_imputed <- predict(imputer, newdata = x_train_mfg)
test_imputed <- predict(imputer, newdata = x_test_mfg)
data.frame(
Set = c("Training", "Test"),
Missing_before = c(sum(is.na(x_train_mfg)), sum(is.na(x_test_mfg))),
Missing_after = c(sum(is.na(train_imputed)), sum(is.na(test_imputed)))
)
## Set Missing_before Missing_after
## 1 Training 81 0
## 2 Test 25 0
Answer: There were 106 missing predictor values. I replaced each missing value with the median of its predictor, calculated from the training data. There are no missing values in the imputed predictor sets. I did not impute the response.
Question: What is the best cross-validation performance for the model I chose?
I use ridge regression because some predictors may be related to each other, and ridge shrinks coefficients without selecting a subset. I remove any predictors that are constant in the fitting data because they provide no information for that fit. I evaluate each candidate lambda using 10-fold cross-validation and select the one with the lowest RMSE.
set.seed(16243)
cv_mfg <- trainControl(method = "cv", number = 10)
ridge_mfg <- train(
x = x_train_mfg, y = y_train_mfg,
method = "glmnet",
preProcess = c("zv", "medianImpute", "center", "scale"),
tuneGrid = ridge_grid,
trControl = cv_mfg,
metric = "RMSE",
standardize = FALSE,
lambda = sort(ridge_grid$lambda, decreasing = TRUE)
)
mfg_best <- ridge_mfg$results[
ridge_mfg$results$lambda == ridge_mfg$bestTune$lambda,
c("lambda", "RMSE", "Rsquared")
]
knitr::kable(mfg_best, digits = 4, row.names = FALSE)
| lambda | RMSE | Rsquared |
|---|---|---|
| 3.9811 | 1.4349 | 0.4823 |
plot(ridge_mfg, main = "Manufacturing yield: tuning ridge")
Answer: The selected lambda is 3.981. The lowest mean cross-validation RMSE is 1.435 percentage points. The corresponding cross-validation R² is 0.482.
Question: What is the test RMSE, and how does it compare with cross-validation RMSE?
mfg_pred <- predict(ridge_mfg, newdata = x_test_mfg)
mfg_test <- postResample(pred = mfg_pred, obs = y_test_mfg)
rmse_difference <- mfg_test[["RMSE"]] - mfg_best$RMSE
knitr::kable(data.frame(
Set = c("Cross-validation", "Test"),
RMSE = c(mfg_best$RMSE, mfg_test[["RMSE"]]),
R2 = c(mfg_best$Rsquared, mfg_test[["Rsquared"]])
), digits = 3)
| Set | RMSE | R2 |
|---|---|---|
| Cross-validation | 1.435 | 0.482 |
| Test | 1.352 | 0.655 |
Answer: The test RMSE is 1.352 percentage points, compared with 1.435 in cross-validation. The test RMSE was 0.083 percentage points lower before rounding the two RMSE values. The test set has 36 runs, so I would not read too much into the difference from this one split.
Question: Which predictors are most important? Do biological or manufacturing process predictors dominate?
I use varImp() on the ridge model. For this model,
predictors are ranked by the absolute sizes of their coefficients at the
selected lambda. Centering and scaling the predictors before fitting
makes the coefficient sizes more comparable.
importance <- varImp(ridge_mfg, scale = FALSE)$importance
importance <- importance[order(importance$Overall, decreasing = TRUE), ,
drop = FALSE]
top_names <- head(rownames(importance), 6)
top_predictors <- data.frame(
Predictor = top_names,
Type = ifelse(grepl("^BiologicalMaterial", top_names),
"Biological", "Manufacturing process"),
Importance = importance[top_names, "Overall"]
)
process_count <- sum(top_predictors$Type == "Manufacturing process")
knitr::kable(top_predictors, digits = 3, row.names = FALSE)
| Predictor | Type | Importance |
|---|---|---|
| ManufacturingProcess32 | Manufacturing process | 0.184 |
| ManufacturingProcess09 | Manufacturing process | 0.137 |
| ManufacturingProcess13 | Manufacturing process | 0.137 |
| ManufacturingProcess36 | Manufacturing process | 0.134 |
| ManufacturingProcess17 | Manufacturing process | 0.128 |
| ManufacturingProcess33 | Manufacturing process | 0.093 |
Answer: The highest-ranked predictor is ManufacturingProcess32. Of the top six, 6 are manufacturing process predictors and 0 are biological predictors. Manufacturing process predictors dominate this list. This ranking measures importance to the fitted model; it does not show which variables cause yield to change.
Question: How does yield relate to each top predictor, and how could the relationships help improve yield?
I plot the observed training values for each top predictor against yield. I leave out missing pairs rather than plotting imputed values as if they were actual measurements. The orange line shows the trend in the observed points.
correlations <- numeric(length(top_names))
old_par <- par(mfrow = c(2, 3), mar = c(4, 4, 2.8, 0.7))
for (i in seq_along(top_names)) {
variable <- top_names[i]
observed <- !is.na(x_train_mfg[[variable]])
x <- x_train_mfg[[variable]][observed]
y <- y_train_mfg[observed]
plot(x, y, pch = 19, cex = 0.6, col = "steelblue",
xlab = "Predictor value", ylab = "Yield (%)",
main = variable, cex.main = 0.8)
lines(lowess(x, y), col = "darkorange3", lwd = 2)
correlations[i] <- cor(x, y)
}
par(old_par)
knitr::kable(data.frame(
Predictor = top_names,
Correlation_with_yield = correlations,
Direction = ifelse(correlations > 0, "Positive",
ifelse(correlations < 0, "Negative", "Zero"))
), digits = 3, row.names = FALSE)
| Predictor | Correlation_with_yield | Direction |
|---|---|---|
| ManufacturingProcess32 | 0.617 | Positive |
| ManufacturingProcess09 | 0.483 | Positive |
| ManufacturingProcess13 | -0.477 | Negative |
| ManufacturingProcess36 | -0.547 | Negative |
| ManufacturingProcess17 | -0.371 | Negative |
| ManufacturingProcess33 | 0.454 | Positive |
Answer: The table shows the direction for each predictor. The strongest observed relationship by absolute correlation was for ManufacturingProcess32 (correlation 0.617). A positive relationship means higher predictor values tended to occur with higher yield; a negative one means they tended to occur with lower yield.
I would use the process variables to decide which settings to investigate in controlled trial runs. For biological variables, I would use any useful relationships to screen or monitor incoming material, since these measurements cannot be directly changed during production. The patterns are associations, so I would not change operating settings based only on these plots.
Kuhn, M., & Johnson, K. (2013). Applied Predictive Modeling. Springer. Chapter 6, exercises 6.2 and 6.3.