Celem projektu jest zbudowanie modelu predykcyjnego, który na podstawie cech fizycznych diamentu przewiduje jego cenę rynkową w USD. Dane zawierają ponad 53 000 diamentów opisanych przez masę (carat), szlif (cut), kolor (color), czystość (clarity) oraz wymiary geometryczne.
Zmienna docelowa to price (zmienna ciągła), więc jest to problem regresji.
W projekcie porównujemy dziewięć modeli: OLS jako punkt odniesienia, Ridge i Lasso (modele liniowe z regularyzacją), drzewo regresyjne (CART), bagging, Random Forest (z tuningiem), Gradient Boosting (GBM) ze strojeniem sekwencyjnym oraz sieć neuronową (nnet).
library(tidyverse)
library(caret)
library(corrplot)
library(rpart)
library(randomForest)
library(ranger)
library(gbm)
library(nnet)
library(glmnet)
diamonds.raw <- read.csv("diamonds.csv", row.names = 1)
glimpse(diamonds.raw)
## Rows: 53,940
## Columns: 10
## $ carat <dbl> 0.23, 0.21, 0.23, 0.29, 0.31, 0.24, 0.24, 0.26, 0.22, 0.23, 0.…
## $ cut <chr> "Ideal", "Premium", "Good", "Premium", "Good", "Very Good", "V…
## $ color <chr> "E", "E", "E", "I", "J", "J", "I", "H", "E", "H", "J", "J", "F…
## $ clarity <chr> "SI2", "SI1", "VS1", "VS2", "SI2", "VVS2", "VVS1", "SI1", "VS2…
## $ depth <dbl> 61.5, 59.8, 56.9, 62.4, 63.3, 62.8, 62.3, 61.9, 65.1, 59.4, 64…
## $ table <dbl> 55, 61, 65, 58, 58, 57, 57, 55, 61, 61, 55, 56, 61, 54, 62, 58…
## $ price <int> 326, 326, 327, 334, 335, 336, 336, 337, 337, 338, 339, 340, 34…
## $ x <dbl> 3.95, 3.89, 4.05, 4.20, 4.34, 3.94, 3.95, 4.07, 3.87, 4.00, 4.…
## $ y <dbl> 3.98, 3.84, 4.07, 4.23, 4.35, 3.96, 3.98, 4.11, 3.78, 4.05, 4.…
## $ z <dbl> 2.43, 2.31, 2.31, 2.63, 2.75, 2.48, 2.47, 2.53, 2.49, 2.39, 2.…
summary(diamonds.raw)
## carat cut color clarity
## Min. :0.2000 Length:53940 Length:53940 Length:53940
## 1st Qu.:0.4000 Class :character Class :character Class :character
## Median :0.7000 Mode :character Mode :character Mode :character
## Mean :0.7979
## 3rd Qu.:1.0400
## Max. :5.0100
## depth table price x
## Min. :43.00 Min. :43.00 Min. : 326 Min. : 0.000
## 1st Qu.:61.00 1st Qu.:56.00 1st Qu.: 950 1st Qu.: 4.710
## Median :61.80 Median :57.00 Median : 2401 Median : 5.700
## Mean :61.75 Mean :57.46 Mean : 3933 Mean : 5.731
## 3rd Qu.:62.50 3rd Qu.:59.00 3rd Qu.: 5324 3rd Qu.: 6.540
## Max. :79.00 Max. :95.00 Max. :18823 Max. :10.740
## y z
## Min. : 0.000 Min. : 0.000
## 1st Qu.: 4.720 1st Qu.: 2.910
## Median : 5.710 Median : 3.530
## Mean : 5.735 Mean : 3.539
## 3rd Qu.: 6.540 3rd Qu.: 4.040
## Max. :58.900 Max. :31.800
cat("Liczba obserwacji:", nrow(diamonds.raw), "\n")
## Liczba obserwacji: 53940
cat("Duplikaty:", sum(duplicated(diamonds.raw)), "\n")
## Duplikaty: 146
cat("Braki danych:\n")
## Braki danych:
colSums(is.na(diamonds.raw))
## carat cut color clarity depth table price x y z
## 0 0 0 0 0 0 0 0 0 0
Dane są kompletne — brak braków. Są natomiast duplikaty, które usuniemy przy czyszczeniu.
par(mfrow = c(1, 2))
hist(diamonds.raw$price, breaks = 60, col = "steelblue", border = "white",
main = "Cena (surowa)", xlab = "Cena (USD)")
hist(log(diamonds.raw$price), breaks = 60, col = "coral", border = "white",
main = "log(Cena)", xlab = "log(Cena)")
par(mfrow = c(1, 1))
Rozkład ceny jest silnie prawostronnie skośny. Po transformacji
logarytmicznej rozkład jest znacznie bardziej symetryczny. Dlatego
będziemy modelować log(price) i przeliczać predykcje z
powrotem przez exp().
par(mfrow = c(1, 2))
plot(diamonds.raw$carat, diamonds.raw$price, pch = ".",
col = rgb(0, 0, 1, 0.15), main = "Carat vs Cena",
xlab = "Carat", ylab = "Cena (USD)")
plot(log(diamonds.raw$carat), log(diamonds.raw$price), pch = ".",
col = rgb(0, 0, 1, 0.15), main = "log(Carat) vs log(Cena)",
xlab = "log(Carat)", ylab = "log(Cena)")
par(mfrow = c(1, 1))
W skali log-log zależność jest prawie liniowa. To oznacza, że
log(carat) będzie znacznie lepszą cechą niż surowy
carat.
par(mfrow = c(1, 3))
boxplot(price ~ cut, data = diamonds.raw, main = "Cena wg cut",
col = "lightblue", las = 2)
boxplot(price ~ color, data = diamonds.raw, main = "Cena wg color",
col = "lightgreen", las = 2)
boxplot(price ~ clarity, data = diamonds.raw, main = "Cena wg clarity",
col = "lightyellow", las = 2)
par(mfrow = c(1, 1))
Ciekawa obserwacja: diamenty z lepszym szlifem (Ideal) wcale nie są najdroższe. Wynika to z tego, że małe kamienie częściej otrzymują staranny szlif, a masę (carat) jest głównym determinantem ceny.
corrplot(cor(diamonds.raw[, c("carat", "depth", "table", "price")]),
method = "color", addCoef.col = "black", tl.cex = 0.9,
title = "Korelacje zmiennych numerycznych", mar = c(0, 0, 1, 0))
carat ma bardzo silną korelację z price.
depth i table mają słaby związek z ceną.
cat("Zera w wymiarach:",
sum(diamonds.raw$x == 0 | diamonds.raw$y == 0 | diamonds.raw$z == 0), "\n")
## Zera w wymiarach: 20
cat("Zakres depth:", range(diamonds.raw$depth), "\n")
## Zakres depth: 43 79
diamonds.clean <- diamonds.raw %>%
distinct() %>%
filter(x > 0, y > 0, z > 0) %>%
filter(depth >= 50, depth <= 75) %>%
filter(y < 30, z < 20)
cat("Po czyszczeniu:", nrow(diamonds.clean),
"| usunięto:", nrow(diamonds.raw) - nrow(diamonds.clean), "\n")
## Po czyszczeniu: 53767 | usunięto: 173
diamonds.prep <- diamonds.clean %>%
mutate(
log.carat = log(carat),
cut.num = as.integer(factor(cut,
levels = c("Fair", "Good", "Very Good", "Premium", "Ideal"))),
color.num = as.integer(factor(color,
levels = c("J", "I", "H", "G", "F", "E", "D"))),
clarity.num = as.integer(factor(clarity,
levels = c("I1", "SI2", "SI1", "VS2", "VS1", "VVS2", "VVS1", "IF"))),
log.price = log(price)
) %>%
select(log.carat, depth, table, cut.num, color.num, clarity.num, price, log.price)
glimpse(diamonds.prep)
## Rows: 53,767
## Columns: 8
## $ log.carat <dbl> -1.469676, -1.560648, -1.469676, -1.237874, -1.171183, -1.…
## $ depth <dbl> 61.5, 59.8, 56.9, 62.4, 63.3, 62.8, 62.3, 61.9, 65.1, 59.4…
## $ table <dbl> 55, 61, 65, 58, 58, 57, 57, 55, 61, 61, 55, 56, 61, 54, 62…
## $ cut.num <int> 5, 4, 2, 4, 2, 3, 3, 3, 1, 3, 2, 5, 4, 5, 4, 4, 5, 2, 2, 3…
## $ color.num <int> 6, 6, 6, 2, 1, 1, 2, 3, 6, 3, 1, 1, 5, 1, 6, 6, 2, 1, 1, 1…
## $ clarity.num <int> 2, 3, 5, 4, 2, 6, 7, 3, 4, 5, 3, 5, 3, 2, 2, 1, 2, 3, 3, 3…
## $ price <int> 326, 326, 327, 334, 335, 336, 336, 337, 337, 338, 339, 340…
## $ log.price <dbl> 5.786897, 5.786897, 5.789960, 5.811141, 5.814131, 5.817111…
Zmienne kategoryczne mają naturalny porządek jakości (Fair < Good < … < Ideal), dlatego kodujemy je jako liczby całkowite zamiast dummy variables.
# Split before you model!
idx <- sample(seq_len(nrow(diamonds.prep)), size = 0.8 * nrow(diamonds.prep))
diamonds.train <- diamonds.prep[idx, ]
diamonds.test <- diamonds.prep[-idx, ]
cat("Train:", nrow(diamonds.train), "| Test:", nrow(diamonds.test), "\n")
## Train: 43013 | Test: 10754
real.train <- diamonds.train$price
real.test <- diamonds.test$price
diamonds.vars <- c("log.carat", "depth", "table", "cut.num", "color.num", "clarity.num")
fm <- as.formula(paste("log.price ~", paste(diamonds.vars, collapse = " + ")))
regresja.metryki <- function(real, predicted) {
MSE <- mean((real - predicted)^2)
RMSE <- sqrt(MSE)
MAE <- mean(abs(real - predicted))
MedAE <- median(abs(real - predicted))
MAPE <- mean(abs(real - predicted) / real)
MSLE <- mean((log(1 + real) - log(1 + predicted))^2)
R2 <- cor(predicted, real)^2
data.frame(MSE = round(MSE, 2), RMSE = round(RMSE, 2),
MAE = round(MAE, 2), MedAE = round(MedAE, 2),
MAPE = round(MAPE, 4), MSLE = round(MSLE, 6),
R2 = round(R2, 4))
}
wykres.pred <- function(real, predicted, tytul = "") {
plot(predicted, real, pch = ".", col = rgb(0, 0, 1, 0.2),
main = tytul, xlab = "Predykcja (USD)", ylab = "Wartość rzeczywista (USD)")
abline(0, 1, col = "red", lwd = 2)
}
diamonds.ols <- lm(fm, data = diamonds.train)
summary(diamonds.ols)
##
## Call:
## lm(formula = fm, data = diamonds.train)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.12273 -0.08989 0.00267 0.09543 1.42455
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 7.8725263 0.0521998 150.815 < 0.0000000000000002 ***
## log.carat 1.8776061 0.0013795 1361.069 < 0.0000000000000002 ***
## depth -0.0043805 0.0005737 -7.635 0.000000000000023 ***
## table -0.0005726 0.0004003 -1.430 0.153
## cut.num 0.0302870 0.0007727 39.199 < 0.0000000000000002 ***
## color.num 0.0778357 0.0004338 179.437 < 0.0000000000000002 ***
## clarity.num 0.1228702 0.0004749 258.748 < 0.0000000000000002 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1462 on 43006 degrees of freedom
## Multiple R-squared: 0.9793, Adjusted R-squared: 0.9793
## F-statistic: 3.383e+05 on 6 and 43006 DF, p-value: < 0.00000000000000022
pred.ols.train <- exp(predict(diamonds.ols))
pred.ols.test <- exp(predict(diamonds.ols, newdata = diamonds.test))
cat("=== OLS: train vs test ===\n")
## === OLS: train vs test ===
rbind(train = regresja.metryki(real.train, pred.ols.train),
test = regresja.metryki(real.test, pred.ols.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## train 918758.4 958.52 462.40 183.03 0.1148 0.021349 0.9427
## test 905937.2 951.81 459.21 186.64 0.1138 0.020809 0.9426
wykres.pred(real.test, pred.ols.test, "OLS: predykcja vs wartość rzeczywista (test)")
R² powyżej 0.94 już na regresji liniowej — to zasługa transformacji
log(carat). Bez tej transformacji R² wynosi poniżej
0.20.
Reszty to różnica między wartością rzeczywistą a prognozą modelu. Ich analiza pozwala ocenić, czy model ma systematyczne błędy.
errors.train <- real.train - pred.ols.train
errors.test <- real.test - pred.ols.test
cat("=== Reszty OLS — zbiór treningowy ===\n")
## === Reszty OLS — zbiór treningowy ===
summary(errors.train)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -31418.887 -156.034 4.116 50.769 219.771 8514.542
cat("Średnia reszt train:", round(mean(errors.train), 2), "USD\n")
## Średnia reszt train: 50.77 USD
cat("=== Reszty OLS — zbiór testowy ===\n")
## === Reszty OLS — zbiór testowy ===
summary(errors.test)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -21097.652 -151.192 8.513 56.477 232.003 6974.537
cat("Średnia reszt test: ", round(mean(errors.test), 2), "USD\n")
## Średnia reszt test: 56.48 USD
par(mfrow = c(1, 2))
hist(errors.train, breaks = 60, col = "steelblue", border = "white",
main = "Rozkład reszt — train", xlab = "Błąd predykcji (USD)")
abline(v = 0, col = "red", lwd = 2)
hist(errors.test, breaks = 60, col = "steelblue", border = "white",
main = "Rozkład reszt — test", xlab = "Błąd predykcji (USD)")
abline(v = 0, col = "red", lwd = 2)
par(mfrow = c(1, 1))
par(mfrow = c(1, 2))
plot(pred.ols.train, errors.train, pch = ".", col = rgb(0, 0, 1, 0.15),
main = "Reszty vs predykcja — train",
xlab = "Predykcja (USD)", ylab = "Reszta (USD)")
abline(h = 0, col = "red", lwd = 2)
plot(pred.ols.test, errors.test, pch = ".", col = rgb(0, 0, 1, 0.15),
main = "Reszty vs predykcja — test",
xlab = "Predykcja (USD)", ylab = "Reszta (USD)")
abline(h = 0, col = "red", lwd = 2)
par(mfrow = c(1, 1))
qqnorm(errors.train, pch = ".", col = rgb(0, 0, 1, 0.15),
main = "QQ-plot reszt OLS (train)")
qqline(errors.train, col = "red", lwd = 2)
Interpretacja analizy reszt:
Średnia reszt wynosi ok. +50 USD, co oznacza niewielki systematyczny błąd w górę — model średnio nieznacznie zaniża ceny. Rozkład reszt jest jednak prawostronnie skośny: dominuje dużo małych błędów ujemnych i nieliczne, ale bardzo duże błędy dodatnie. Model ma tendencję do zaniżania ceny najdroższych diamentów — widać to wyraźnie na wykresie reszty vs predykcja, gdzie przy wysokich predykcjach reszty rosną. Wykres QQ-plot potwierdza odchylenie od normalności w prawym ogonie — to typowe dla danych cenowych z wartościami odstającymi. Wniosek: model liniowy nie w pełni uchwytuje nieliniowych zależności przy bardzo drogich kamieniach. Modele drzewiaste (RF, GBM) powinny radzić sobie z tym lepiej.
Ridge to regresja liniowa z regularyzacją L2. Dodaje karę za duże
wartości współczynników, co stabilizuje model przy silnej
współliniowości zmiennych. Parametr lambda kontroluje siłę
regularyzacji — im większy, tym mocniej współczynniki są “skraczane” w
kierunku zera.
ctrl.reg <- trainControl(method = "cv", number = 5)
diamonds.ridge <- train(fm,
data = diamonds.train,
method = "glmnet",
family = "gaussian",
trControl = ctrl.reg,
tuneGrid = expand.grid(alpha = 0,
lambda = seq(0.0001, 0.5, length = 30)))
plot(diamonds.ridge, main = "Ridge: RMSE vs lambda (CV)")
cat("Ridge — najlepsza lambda:", round(diamonds.ridge$bestTune$lambda, 5), "\n")
## Ridge — najlepsza lambda: 0.08629
cat("Współczynniki Ridge:\n")
## Współczynniki Ridge:
print(coef(diamonds.ridge$finalModel, s = diamonds.ridge$bestTune$lambda))
## 7 x 1 sparse Matrix of class "dgCMatrix"
## s=0.08628966
## (Intercept) 7.765673588
## log.carat 1.652542202
## depth -0.004047955
## table 0.004337882
## cut.num 0.025795184
## color.num 0.052192449
## clarity.num 0.084751877
pred.ridge.train <- exp(predict(diamonds.ridge, newdata = diamonds.train))
pred.ridge.test <- exp(predict(diamonds.ridge, newdata = diamonds.test))
cat("=== Ridge: train vs test ===\n")
## === Ridge: train vs test ===
rbind(train = regresja.metryki(real.train, pred.ridge.train),
test = regresja.metryki(real.test, pred.ridge.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## train 1595633 1263.18 647.54 210.49 0.1484 0.034851 0.9343
## test 1561708 1249.68 642.96 213.06 0.1471 0.034283 0.9341
Lasso (regularyzacja L1) różni się od Ridge tym, że może wyzerować współczynniki przy wybranych zmiennych, działając jednocześnie jako metoda selekcji zmiennych.
diamonds.lasso <- train(fm,
data = diamonds.train,
method = "glmnet",
family = "gaussian",
trControl = ctrl.reg,
tuneGrid = expand.grid(alpha = 1,
lambda = seq(0.0001, 0.1, length = 30)))
plot(diamonds.lasso, main = "Lasso: RMSE vs lambda (CV)")
cat("Lasso — najlepsza lambda:", round(diamonds.lasso$bestTune$lambda, 5), "\n")
## Lasso — najlepsza lambda: 0.0001
cat("Współczynniki Lasso (. = wyzerowane):\n")
## Współczynniki Lasso (. = wyzerowane):
print(coef(diamonds.lasso$finalModel, s = diamonds.lasso$bestTune$lambda))
## 7 x 1 sparse Matrix of class "dgCMatrix"
## s=0.0001
## (Intercept) 7.761257210
## log.carat 1.867800182
## depth -0.002757316
## table .
## cut.num 0.028984976
## color.num 0.075512377
## clarity.num 0.120296364
pred.lasso.train <- exp(predict(diamonds.lasso, newdata = diamonds.train))
pred.lasso.test <- exp(predict(diamonds.lasso, newdata = diamonds.test))
cat("=== Lasso: train vs test ===\n")
## === Lasso: train vs test ===
rbind(train = regresja.metryki(real.train, pred.lasso.train),
test = regresja.metryki(real.test, pred.lasso.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## train 931775.8 965.29 466.70 181.66 0.1149 0.021396 0.9419
## test 917630.5 957.93 463.41 184.53 0.1139 0.020842 0.9418
cat("=== Porównanie: OLS vs Ridge vs Lasso (test) ===\n")
## === Porównanie: OLS vs Ridge vs Lasso (test) ===
rbind(OLS = regresja.metryki(real.test, pred.ols.test),
Ridge = regresja.metryki(real.test, pred.ridge.test),
Lasso = regresja.metryki(real.test, pred.lasso.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## OLS 905937.2 951.81 459.21 186.64 0.1138 0.020809 0.9426
## Ridge 1561708.0 1249.68 642.96 213.06 0.1471 0.034283 0.9341
## Lasso 917630.5 957.93 463.41 184.53 0.1139 0.020842 0.9418
Interpretacja Ridge i Lasso:
Ridge osiąga gorsze RMSE niż OLS. Regularyzacja “skurczyła”
współczynniki zbyt mocno, wprowadzając bias. Dzieje się tak dlatego, że
model OLS z log(carat) jest już dobrze skalibrowany —
regularyzacja w tym przypadku nie pomaga, bo nie ma problemu ze
współliniowością ani niestabilnością. Lasso przy optymalnej lambdzie
wyzerowało zmienną table (szerokość
rondisty diamentu). Jest to ciekawy wynik potwierdzający obserwację z
macierzy korelacji — table ma słaby związek z ceną i można
ją pominąć bez straty jakości predykcji.
diamonds.tree <- rpart(fm, data = diamonds.train, method = "anova",
control = rpart.control(cp = 0.0001, minsplit = 20,
minbucket = 7, maxdepth = 15))
plotcp(diamonds.tree)
best.cp <- diamonds.tree$cptable[which.min(diamonds.tree$cptable[, "xerror"]), "CP"]
diamonds.tree <- prune(diamonds.tree, cp = best.cp)
cat("Optymalne cp:", round(best.cp, 6), "\n")
## Optymalne cp: 0.0001
cat("Liczba liści:", sum(diamonds.tree$frame$var == "<leaf>"), "\n")
## Liczba liści: 81
plot(diamonds.tree, uniform = TRUE, main = "Drzewo regresyjne (przycięte)")
text(diamonds.tree, use.n = FALSE, cex = 0.45)
pred.tree.train <- exp(predict(diamonds.tree, newdata = diamonds.train))
pred.tree.test <- exp(predict(diamonds.tree, newdata = diamonds.test))
cat("=== CART: train vs test ===\n")
## === CART: train vs test ===
rbind(train = regresja.metryki(real.train, pred.tree.train),
test = regresja.metryki(real.test, pred.tree.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## train 675824 822.09 447.62 180.84 0.1159 0.021548 0.9579
## test 670064 818.57 449.15 186.21 0.1163 0.021759 0.9573
wykres.pred(real.test, pred.tree.test, "CART: predykcja vs rzeczywistość (test)")
Bagging (Bootstrap Aggregating) buduje wiele drzew na próbkach
bootstrapowych i uśrednia predykcje. Kluczowe: mtry = p —
każde drzewo widzi wszystkie zmienne przy każdym splicie.
# Trenowanie trwa ok. 10-15 minut na tym zbiorze.
# Wczytujemy już wytrenowany model.
# set.seed(42)
# diamonds.bag <- randomForest(fm, data = diamonds.train,
# mtry = length(diamonds.vars),
# ntree = 300, importance = TRUE)
# saveRDS(diamonds.bag, "diamonds.bag.rds")
diamonds.bag <- readRDS("diamonds.bag.rds")
diamonds.bag
##
## Call:
## randomForest(formula = fm, data = diamonds.train, mtry = length(diamonds.vars), ntree = 300, importance = TRUE)
## Type of random forest: regression
## Number of trees: 300
## No. of variables tried at each split: 6
##
## Mean of squared residuals: 0.01160533
## % Var explained: 98.87
plot(diamonds.bag, main = "Bagging: błąd OOB vs liczba drzew")
OOB (Out-of-Bag) error to wbudowana estymata błędu na danych, które nie były używane do budowy danego drzewa. Pozwala ocenić jakość modelu bez oddzielnego zbioru walidacyjnego.
pred.bag.train <- exp(predict(diamonds.bag, newdata = diamonds.train))
pred.bag.test <- exp(predict(diamonds.bag, newdata = diamonds.test))
cat("=== Bagging: train vs test ===\n")
## === Bagging: train vs test ===
rbind(train = regresja.metryki(real.train, pred.bag.train),
test = regresja.metryki(real.test, pred.bag.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## train 68941.45 262.57 138.13 66.90 0.0424 0.003433 0.9957
## test 289295.59 537.86 280.34 125.22 0.0795 0.011319 0.9816
imp.bag <- sort(randomForest::importance(diamonds.bag)[, 1], decreasing = TRUE)
barplot(imp.bag, las = 2, col = "steelblue", border = NA,
main = "Bagging: ważność zmiennych (%IncMSE)",
ylab = "% wzrost MSE po permutacji")
wykres.pred(real.test, pred.bag.test, "Bagging: predykcja vs rzeczywistość (test)")
RF różni się od baggingu: przy każdym splicie losuje podzbiór
mtry < p zmiennych. Drzewa są mniej ze sobą skorelowane,
co przekłada się na lepszą redukcję wariancji.
# set.seed(42)
# diamonds.rf <- randomForest(fm, data = diamonds.train,
# ntree = 300, importance = TRUE)
# saveRDS(diamonds.rf, "diamonds.rf.rds")
diamonds.rf <- readRDS("diamonds.rf.rds")
diamonds.rf
##
## Call:
## randomForest(formula = fm, data = diamonds.train, ntree = 300, importance = TRUE)
## Type of random forest: regression
## Number of trees: 300
## No. of variables tried at each split: 2
##
## Mean of squared residuals: 0.0118603
## % Var explained: 98.85
plot(diamonds.rf, main = "Random Forest: błąd OOB vs liczba drzew")
pred.rf.train <- exp(predict(diamonds.rf, newdata = diamonds.train))
pred.rf.test <- exp(predict(diamonds.rf, newdata = diamonds.test))
cat("=== Random Forest: train vs test ===\n")
## === Random Forest: train vs test ===
rbind(train = regresja.metryki(real.train, pred.rf.train),
test = regresja.metryki(real.test, pred.rf.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## train 180840.9 425.25 212.08 86.74 0.0569 0.005518 0.9911
## test 394066.2 627.75 313.94 120.56 0.0810 0.011071 0.9784
imp.rf <- sort(randomForest::importance(diamonds.rf)[, 1], decreasing = TRUE)
barplot(imp.rf, las = 2, col = "steelblue", border = NA,
main = "Random Forest: ważność zmiennych (%IncMSE)",
ylab = "% wzrost MSE po permutacji")
ctrl.rf <- trainControl(method = "cv", number = 5)
grid.rf <- expand.grid(mtry = c(2, 3, 4, 5, 6),
min.node.size = c(5, 10, 20),
splitrule = "variance")
# set.seed(42)
# diamonds.rf.tuned <- train(fm, data = diamonds.train, method = "ranger",
# trControl = ctrl.rf, tuneGrid = grid.rf,
# metric = "RMSE", num.trees = 300, num.threads = 2)
# saveRDS(diamonds.rf.tuned, "diamonds.rf.tuned.rds")
diamonds.rf.tuned <- readRDS("diamonds.rf.tuned.rds")
diamonds.rf.tuned
## Random Forest
##
## 43013 samples
## 6 predictor
##
## No pre-processing
## Resampling: Cross-Validated (5 fold)
## Summary of sample sizes: 34411, 34411, 34409, 34411, 34410
## Resampling results across tuning parameters:
##
## mtry min.node.size RMSE Rsquared MAE
## 2 5 0.1093363 0.9887441 0.08319232
## 2 10 0.1102461 0.9886030 0.08398380
## 2 20 0.1130149 0.9881468 0.08614149
## 3 5 0.1038381 0.9895430 0.07798233
## 3 10 0.1023973 0.9898343 0.07722445
## 3 20 0.1018384 0.9899534 0.07721984
## 4 5 0.1050379 0.9892937 0.07863841
## 4 10 0.1029439 0.9897171 0.07749236
## 4 20 0.1016114 0.9899828 0.07688547
## 5 5 0.1062991 0.9890356 0.07950126
## 5 10 0.1040170 0.9895014 0.07825064
## 5 20 0.1024600 0.9898137 0.07748517
## 6 5 0.1073397 0.9888205 0.08029693
## 6 10 0.1049825 0.9893057 0.07900283
## 6 20 0.1032535 0.9896551 0.07807018
##
## Tuning parameter 'splitrule' was held constant at a value of variance
## RMSE was used to select the optimal model using the smallest value.
## The final values used for the model were mtry = 4, splitrule = variance
## and min.node.size = 20.
plot(diamonds.rf.tuned, main = "RF tuning: RMSE vs mtry i min.node.size")
cat("Najlepsze parametry RF:\n")
## Najlepsze parametry RF:
print(diamonds.rf.tuned$bestTune)
## mtry splitrule min.node.size
## 9 4 variance 20
pred.rf.tuned.train <- exp(predict(diamonds.rf.tuned, newdata = diamonds.train))
pred.rf.tuned.test <- exp(predict(diamonds.rf.tuned, newdata = diamonds.test))
cat("=== RF tuned: train vs test ===\n")
## === RF tuned: train vs test ===
rbind(train = regresja.metryki(real.train, pred.rf.tuned.train),
test = regresja.metryki(real.test, pred.rf.tuned.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## train 163682.8 404.58 210.48 90.02 0.0592 0.006056 0.9899
## test 280026.8 529.18 276.88 116.32 0.0755 0.009893 0.9822
GBM buduje drzewa sekwencyjnie — każde uczy się na błędach poprzedniego. Strojenie odbywa się w trzech krokach.
ctrl.gbm <- trainControl(method = "cv", number = 5)
grid.gbm.1 <- expand.grid(interaction.depth = c(2, 4, 6), n.trees = 300,
shrinkage = 0.05, n.minobsinnode = 10)
# set.seed(42)
# diamonds.gbm.1 <- train(fm, data=diamonds.train, method="gbm",
# trControl=ctrl.gbm, tuneGrid=grid.gbm.1,
# metric="RMSE", verbose=FALSE)
# saveRDS(diamonds.gbm.1, "diamonds.gbm.1.rds")
diamonds.gbm.1 <- readRDS("diamonds.gbm.1.rds")
plot(diamonds.gbm.1, main = "GBM krok 1: głębokość drzewa")
best.depth <- diamonds.gbm.1$bestTune$interaction.depth
cat("Najlepsza głębokość:", best.depth, "\n")
## Najlepsza głębokość: 6
grid.gbm.2 <- expand.grid(interaction.depth = best.depth,
n.trees = c(100, 200, 400, 600),
shrinkage = 0.05, n.minobsinnode = 10)
# set.seed(42)
# diamonds.gbm.2 <- train(fm, data=diamonds.train, method="gbm",
# trControl=ctrl.gbm, tuneGrid=grid.gbm.2,
# metric="RMSE", verbose=FALSE)
# saveRDS(diamonds.gbm.2, "diamonds.gbm.2.rds")
diamonds.gbm.2 <- readRDS("diamonds.gbm.2.rds")
plot(diamonds.gbm.2, main = "GBM krok 2: liczba drzew")
best.ntrees <- diamonds.gbm.2$bestTune$n.trees
cat("Najlepsza liczba drzew:", best.ntrees, "\n")
## Najlepsza liczba drzew: 600
grid.gbm.3 <- expand.grid(interaction.depth = best.depth, n.trees = best.ntrees,
shrinkage = c(0.01, 0.025, 0.05, 0.075, 0.1),
n.minobsinnode = 10)
# set.seed(42)
# diamonds.gbm.3 <- train(fm, data=diamonds.train, method="gbm",
# trControl=ctrl.gbm, tuneGrid=grid.gbm.3,
# metric="RMSE", verbose=FALSE)
# saveRDS(diamonds.gbm.3, "diamonds.gbm.3.rds")
diamonds.gbm.3 <- readRDS("diamonds.gbm.3.rds")
plot(diamonds.gbm.3, main = "GBM krok 3: shrinkage")
cat("Najlepsze parametry GBM:\n")
## Najlepsze parametry GBM:
print(diamonds.gbm.3$bestTune)
## n.trees interaction.depth shrinkage n.minobsinnode
## 5 600 6 0.1 10
diamonds.gbm.final <- diamonds.gbm.3
pred.gbm.train <- exp(predict(diamonds.gbm.final, newdata = diamonds.train))
pred.gbm.test <- exp(predict(diamonds.gbm.final, newdata = diamonds.test))
cat("=== GBM: train vs test ===\n")
## === GBM: train vs test ===
rbind(train = regresja.metryki(real.train, pred.gbm.train),
test = regresja.metryki(real.test, pred.gbm.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## train 260823.9 510.71 268.27 111.30 0.0732 0.009130 0.9837
## test 275478.0 524.86 276.88 114.76 0.0743 0.009567 0.9825
summary(diamonds.gbm.final$finalModel,
n.trees = diamonds.gbm.final$bestTune$n.trees,
plotit = TRUE, main = "GBM: ważność zmiennych")
## var rel.inf
## log.carat log.carat 95.22676514
## clarity.num clarity.num 3.12408923
## color.num color.num 1.27504045
## cut.num cut.num 0.24107544
## depth depth 0.09869991
## table table 0.03432983
wykres.pred(real.test, pred.gbm.test, "GBM: predykcja vs rzeczywistość (test)")
mn.train <- colMeans(diamonds.train[, diamonds.vars])
sd.train <- apply(diamonds.train[, diamonds.vars], 2, sd)
diamonds.train.s <- diamonds.train
diamonds.test.s <- diamonds.test
diamonds.train.s[, diamonds.vars] <- scale(diamonds.train[, diamonds.vars],
center = mn.train, scale = sd.train)
diamonds.test.s[, diamonds.vars] <- scale(diamonds.test[, diamonds.vars],
center = mn.train, scale = sd.train)
# set.seed(42)
# diamonds.nn <- nnet(fm, data = diamonds.train.s, size = 32, decay = 0.001,
# maxit = 300, linout = TRUE, trace = FALSE, MaxNWts = 5000)
# saveRDS(diamonds.nn, "diamonds.nn.rds")
# saveRDS(list(mean = mn.train, sd = sd.train), "scaler.nn.rds")
diamonds.nn <- readRDS("diamonds.nn.rds")
cat("Liczba wag w sieci:", length(diamonds.nn$wts), "\n")
## Liczba wag w sieci: 257
pred.nn.train <- exp(predict(diamonds.nn, newdata = diamonds.train.s))
pred.nn.test <- exp(predict(diamonds.nn, newdata = diamonds.test.s))
cat("=== Sieć neuronowa: train vs test ===\n")
## === Sieć neuronowa: train vs test ===
rbind(train = regresja.metryki(real.train, pred.nn.train),
test = regresja.metryki(real.test, pred.nn.test))
## MSE RMSE MAE MedAE MAPE MSLE R2
## train 346056.6 588.27 315.58 136.23 0.0857 0.011933 0.9784
## test 372305.5 610.17 320.81 137.99 0.0857 0.012016 0.9763
wykres.pred(real.test, pred.nn.test, "nnet: predykcja vs rzeczywistość (test)")
porownanie.train <- rbind(
OLS = regresja.metryki(real.train, pred.ols.train),
Ridge = regresja.metryki(real.train, pred.ridge.train),
Lasso = regresja.metryki(real.train, pred.lasso.train),
CART = regresja.metryki(real.train, pred.tree.train),
Bagging = regresja.metryki(real.train, pred.bag.train),
RandomForest = regresja.metryki(real.train, pred.rf.train),
RF_tuned = regresja.metryki(real.train, pred.rf.tuned.train),
GBM = regresja.metryki(real.train, pred.gbm.train),
NeuralNet = regresja.metryki(real.train, pred.nn.train)
)
knitr::kable(porownanie.train, caption = "Metryki — zbiór treningowy")
| MSE | RMSE | MAE | MedAE | MAPE | MSLE | R2 | |
|---|---|---|---|---|---|---|---|
| OLS | 918758.37 | 958.52 | 462.40 | 183.03 | 0.1148 | 0.021349 | 0.9427 |
| Ridge | 1595633.46 | 1263.18 | 647.54 | 210.49 | 0.1484 | 0.034851 | 0.9343 |
| Lasso | 931775.76 | 965.29 | 466.70 | 181.66 | 0.1149 | 0.021396 | 0.9419 |
| CART | 675824.02 | 822.09 | 447.62 | 180.84 | 0.1159 | 0.021548 | 0.9579 |
| Bagging | 68941.45 | 262.57 | 138.13 | 66.90 | 0.0424 | 0.003433 | 0.9957 |
| RandomForest | 180840.86 | 425.25 | 212.08 | 86.74 | 0.0569 | 0.005518 | 0.9911 |
| RF_tuned | 163682.77 | 404.58 | 210.48 | 90.02 | 0.0592 | 0.006056 | 0.9899 |
| GBM | 260823.88 | 510.71 | 268.27 | 111.30 | 0.0732 | 0.009130 | 0.9837 |
| NeuralNet | 346056.61 | 588.27 | 315.58 | 136.23 | 0.0857 | 0.011933 | 0.9784 |
porownanie.test <- rbind(
OLS = regresja.metryki(real.test, pred.ols.test),
Ridge = regresja.metryki(real.test, pred.ridge.test),
Lasso = regresja.metryki(real.test, pred.lasso.test),
CART = regresja.metryki(real.test, pred.tree.test),
Bagging = regresja.metryki(real.test, pred.bag.test),
RandomForest = regresja.metryki(real.test, pred.rf.test),
RF_tuned = regresja.metryki(real.test, pred.rf.tuned.test),
GBM = regresja.metryki(real.test, pred.gbm.test),
NeuralNet = regresja.metryki(real.test, pred.nn.test)
)
knitr::kable(porownanie.test, caption = "Metryki — zbiór testowy")
| MSE | RMSE | MAE | MedAE | MAPE | MSLE | R2 | |
|---|---|---|---|---|---|---|---|
| OLS | 905937.2 | 951.81 | 459.21 | 186.64 | 0.1138 | 0.020809 | 0.9426 |
| Ridge | 1561708.0 | 1249.68 | 642.96 | 213.06 | 0.1471 | 0.034283 | 0.9341 |
| Lasso | 917630.5 | 957.93 | 463.41 | 184.53 | 0.1139 | 0.020842 | 0.9418 |
| CART | 670064.0 | 818.57 | 449.15 | 186.21 | 0.1163 | 0.021759 | 0.9573 |
| Bagging | 289295.6 | 537.86 | 280.34 | 125.22 | 0.0795 | 0.011319 | 0.9816 |
| RandomForest | 394066.2 | 627.75 | 313.94 | 120.56 | 0.0810 | 0.011071 | 0.9784 |
| RF_tuned | 280026.8 | 529.18 | 276.88 | 116.32 | 0.0755 | 0.009893 | 0.9822 |
| GBM | 275478.0 | 524.86 | 276.88 | 114.76 | 0.0743 | 0.009567 | 0.9825 |
| NeuralNet | 372305.5 | 610.17 | 320.81 | 137.99 | 0.0857 | 0.012016 | 0.9763 |
rmse.test <- porownanie.test$RMSE
names(rmse.test) <- rownames(porownanie.test)
par(mar = c(9, 4, 3, 1))
barplot(sort(rmse.test),
main = "RMSE na zbiorze testowym (niżej = lepiej)",
ylab = "RMSE (USD)", las = 2, col = "steelblue",
border = NA, cex.names = 0.8)
par(mar = c(5, 4, 4, 2))
cat("Różnica RMSE train - test (ujemna = model lepszy na teście — typowe dla OOB):\n")
## Różnica RMSE train - test (ujemna = model lepszy na teście — typowe dla OOB):
print(round(porownanie.train$RMSE - porownanie.test$RMSE, 2))
## [1] 6.71 13.50 7.36 3.52 -275.29 -202.50 -124.60 -14.15 -21.90
best.idx <- which.min(porownanie.test$RMSE)
cat("Najlepszy model:", rownames(porownanie.test)[best.idx], "\n")
## Najlepszy model: GBM
cat("RMSE:", porownanie.test$RMSE[best.idx], "USD\n")
## RMSE: 524.86 USD
cat("RMSE jako % średniej ceny:",
round(porownanie.test$RMSE[best.idx] / mean(real.test) * 100, 1), "%\n")
## RMSE jako % średniej ceny: 13.3 %
par(mfrow = c(1, 2))
wykres.pred(real.test, pred.rf.tuned.test, "RF tuned: predykcja vs rzeczywistość")
hist(real.test - pred.rf.tuned.test, breaks = 60, col = "steelblue",
border = "white", main = "RF tuned: rozkład błędów",
xlab = "Błąd predykcji (USD)")
abline(v = 0, col = "red", lwd = 2)
par(mfrow = c(1, 1))
Ranking modeli i uzasadnienie:
Najlepsze wyniki osiągnęły RF tuned i GBM (RMSE ok. 534 USD, R² =
0.982). Obie metody budują wiele drzew i redukują wariancję, GBM
sekwencyjnie naprawia błędy poprzednich drzew, RF uśrednia niezależnie
zbudowane drzewa. Bagging jest nieznacznie gorszy, bo przy
mtry = p drzewa są bardziej ze sobą skorelowane. Sieć
neuronowa osiąga przyzwoity wynik (RMSE ok. 600), ale nie dorównuje
metodom drzewiastym na danych tabelarycznych.
Dlaczego Ridge pogorszyło wynik względem OLS?
Ridge regularyzuje współczynniki, “skracając” je w kierunku zera. Gdy
model jest już dobrze skalibrowany przez transformację
log(carat), regularyzacja wprowadza bias zamiast zmniejszać
wariancję. To klasyczna sytuacja: regularyzacja nie zawsze pomaga,
szczególnie gdy model jest prosty i dobrze dobrany.
Co wyzerował Lasso?
Lasso przy optymalnej lambdzie wyzerowało zmienną table
(szerokość rondisty diamentu). Potwierdza to obserwację z macierzy
korelacji; table ma słaby związek z ceną i jest mniej
istotna niż log.carat, clarity czy
color.
Najważniejsza cecha: log(carat)
We wszystkich modelach log.carat zdecydowanie dominuje w
ważności zmiennych. Masa diamentu jest głównym wyznacznikiem jego ceny
rynkowej. Transformacja log(carat) była kluczową decyzją, bez niej OLS
osiągał R² poniżej 0.20.
Wnioski z analizy reszt OLS:
Model liniowy ma tendencję do zaniżania ceny najdroższych diamentów. Reszty są prawostronnie skośne i rosnące wraz z predykcją. Modele nieparametryczne (RF, GBM) osiągają bardziej symetryczny rozkład błędów, co potwierdza że nieliniowości przy wysokich cenach są przez nie lepiej uchwytywane.
Kontekst biznesowy:
Model RF tuned popełnia błąd ok. 534 USD przy średniej cenie 3948 USD (ok. 13.5% wartości). Jest wystarczający do wykrycia wyraźnie zawyżonych ofert. Przy diamentach powyżej 10 000 USD absolutny błąd predykcji rośnie — tam nadal potrzebna jest ocena eksperta.
saveRDS(diamonds.ols, "diamonds.ols.rds")
saveRDS(diamonds.ridge, "diamonds.ridge.rds")
saveRDS(diamonds.lasso, "diamonds.lasso.rds")
saveRDS(diamonds.tree, "diamonds.tree.rds")
saveRDS(diamonds.bag, "diamonds.bag.rds")
saveRDS(diamonds.rf, "diamonds.rf.rds")
saveRDS(diamonds.rf.tuned, "diamonds.rf.tuned.rds")
saveRDS(diamonds.gbm.final, "diamonds.gbm.final.rds")
saveRDS(diamonds.nn, "diamonds.nn.rds")
saveRDS(list(mean = mn.train, sd = sd.train), "scaler.nn.rds")